From 36e2124855309a606968a021663f782ccd9edae8 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 05:44:29 -0800 Subject: [PATCH] Status bar: a line about linked files keeps the open's notes listed When a document opened with notes and with linked files that changed on disk (Update Links: Manually) or, on the web, can't be read, the line about the linked files replaced the "Opened with N notes" line, and the notes could no longer be listed on hover or by a screen reader. The status bar now shows the line about the linked files, then counts the notes and names the first, and keeps the notes for that line. - The Linked File Not Found and Find Linked Files dialogs keep the open's notes (field `notes`), so the line about modified files shown after them carries the notes of that open, not of an earlier one. - File > New from Template... shows the notes of the file it opens, as File > Open does. - New message "{notice}. Opened with {n} note(s): {first}" in every catalog that translates "Opened with {n} note(s): {first}". Docs: docs/control-protocol.md, docs/target-app-parity.md. --- crates/ui-egui/src/chrome/tests.rs | 26 ++++ crates/ui-egui/src/dialogs/find_links.rs | 8 +- .../ui-egui/src/dialogs/find_links/tests.rs | 9 +- crates/ui-egui/src/dialogs/missing_links.rs | 140 ++++++++++++++---- crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/bs.tsv | 1 + crates/ui-egui/src/i18n/ca.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hi.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tests.rs | 20 ++- crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/io.rs | 49 ++++-- crates/ui-egui/src/io/open.rs | 33 ++++- crates/ui-egui/src/recovery.rs | 2 +- docs/control-protocol.md | 9 +- docs/target-app-parity.md | 1 + 37 files changed, 266 insertions(+), 58 deletions(-) diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index 626ff8c66e..8390085a2c 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -491,6 +491,32 @@ fn the_message_of_an_open_with_several_notes_lists_them_on_hover() { assert_eq!(hovered.iter().filter(|t| t.text.starts_with("Opened")).count(), 1, "no tooltip: {hovered:?}"); } +/// The message of an open that starts with a line about its linked files lists the open's notes on +/// hover and for screen readers, in a wide and in a narrow window. A later message leaves the list +/// out. +#[test] +fn combined_open_and_link_status_lists_notes_on_hover_and_for_accessibility() { + let (mut app, ctx) = status_readout_app(); + ctx.enable_accesskit(); + let notes = vec!["the file's layers weren't read".to_string(), "a brush wasn't read".to_string()]; + let notice = crate::dialogs::missing_links::changed_on_disk_status(2); + let opened = crate::io::opened_with_notes(2, ¬es[0]); + crate::io::show_opened(&mut app, Some(notice.clone()), notes); + assert!(app.ui.status.starts_with(¬ice) && app.ui.status.ends_with(&opened), "{}", app.ui.status); + let full = format!("{}\n• the file's layers weren't read\n• a brush wasn't read", app.ui.status); + for width in [1400.0, 700.0] { + let (texts, nodes, hovered) = bar_hover(&mut app, &ctx, width, "2 linked", super::status_bar); + assert!(width > 700.0 || status_text(&texts, "2 linked").elided, "the narrow window cuts the message short"); + assert!(nodes.iter().any(|n| n.value() == Some(full.as_str())), "screen readers receive the notes at {width}: {nodes:?}"); + assert!(hovered.iter().any(|t| t.text == full), "the tooltip lists the notes at {width}: {hovered:?}"); + } + + app.ui.status = crate::dialogs::missing_links::changed_on_disk_status(1); + let (_, nodes, hovered) = bar_hover(&mut app, &ctx, 1400.0, "1 linked", super::status_bar); + assert!(!nodes.iter().any(|n| n.value().is_some_and(|v| v.contains("• "))), "{nodes:?}"); + assert_eq!(hovered.iter().filter(|t| t.text.starts_with("1 linked")).count(), 1, "no tooltip: {hovered:?}"); +} + /// The list has at most 30 notes, then how many more there are. #[test] fn the_hover_lists_thirty_notes_and_counts_the_rest() { diff --git a/crates/ui-egui/src/dialogs/find_links.rs b/crates/ui-egui/src/dialogs/find_links.rs index 05f0e85b60..e30278959c 100644 --- a/crates/ui-egui/src/dialogs/find_links.rs +++ b/crates/ui-egui/src/dialogs/find_links.rs @@ -11,8 +11,9 @@ //! in Folder… (`ui.findLinksInFolder`), they stay as they are. //! //! Fields: `document` (the document's uid), `missing` (`[{name, path, ids}]`), `modified` (the ids -//! of the images whose file was modified, offered for update afterwards), `back` (the files left -//! without a choice and those that fail to relink go back to dialog `missingLinks`), `state` +//! of the images whose file was modified, offered for update afterwards), `notes` (from +//! `missingLinks`: the open's notes, shown with the line about the modified files), `back` (the +//! files left without a choice and those that fail to relink go back to dialog `missingLinks`), `state` //! (empty, `searching`, `done`, `stopped` or `failed`), `search` (the search's `id`), `folder`, //! `found` (`links.findFiles`'s `links`, with the files found), `searched`, `skipped`, //! `unreadable`, `stopped`, `error`, `picks` (the file chosen for each row of `missing`, or null), @@ -66,6 +67,7 @@ fn dialog(app: &VectorcraftApp, missing: Vec, modified: Vec, back: /// Linked Files, searching `folder` for every file `d` still asks about. pub(super) fn from_missing_links(app: &mut VectorcraftApp, d: &mut Dialog, folder: &str) -> Result<(), String> { let mut find = dialog(app, rows(d, "missing"), rows(d, "modified"), true)?; + find.fields.insert("notes".into(), json!(super::missing_links::notes(d))); start_search(app, &mut find, folder)?; *d = find; Ok(()) @@ -385,7 +387,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { None => {} } if d.bool("back") { - super::missing_links::ask_next(app, rest, rows(d, "modified")); + super::missing_links::ask_next(app, rest, rows(d, "modified"), super::missing_links::notes(d)); } Ok(json!({ "relinked": relinked, "failed": failed })) } diff --git a/crates/ui-egui/src/dialogs/find_links/tests.rs b/crates/ui-egui/src/dialogs/find_links/tests.rs index 4722b8b327..3291524b5e 100644 --- a/crates/ui-egui/src/dialogs/find_links/tests.rs +++ b/crates/ui-egui/src/dialogs/find_links/tests.rs @@ -126,7 +126,8 @@ fn find_in_folder_relinks_the_missing_files_to_the_files_found() { } /// The files left without a file, or whose file fails to relink, go back to Linked File Not -/// Found; the failure is named in the status line, and the other files still relink. +/// Found, with the open's notes; the failure is named in the status line, and the other files +/// still relink. #[test] fn files_not_relinked_go_back_to_linked_file_not_found() { let root = folder("back"); @@ -134,10 +135,13 @@ fn files_not_relinked_go_back_to_linked_file_not_found() { let bytes = opened_without(&mut app, &root, &["a.png", "b.png", "c.png"]); write(&at(&root, "found/a.png"), &bytes[0]); write(&at(&root, "found/b.png"), b"not an image"); - app.ui.dialog.as_mut().unwrap().fields.insert("searchFolder".into(), json!(root.join("found"))); + let missing_links = app.ui.dialog.as_mut().unwrap(); + missing_links.fields.insert("notes".into(), json!(["a note"])); + missing_links.fields.insert("searchFolder".into(), json!(root.join("found"))); super::super::confirm(&mut app).unwrap(); searched(&mut app); let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!(d.fields["notes"], json!(["a note"])); // b.png's only file is chosen, c.png has none. assert_eq!((pick(d, "b.png"), pick(d, "c.png")), (&json!(at(&root, "found/b.png")), &Value::Null)); assert!(frame(&mut app).contains("Not found")); @@ -147,6 +151,7 @@ fn files_not_relinked_go_back_to_linked_file_not_found() { let names: Vec<&str> = d.fields["missing"].as_array().unwrap().iter().map(|r| r["name"].as_str().unwrap()).collect(); assert_eq!((d.kind.as_str(), names.len()), (super::super::missing_links::KIND, 2)); assert!(names.contains(&"b.png") && names.contains(&"c.png"), "{names:?}"); + assert_eq!(d.fields["notes"], json!(["a note"])); super::super::cancel(&mut app); assert_eq!(app.run("links.check", json!({})).unwrap()["missing"], 2); } diff --git a/crates/ui-egui/src/dialogs/missing_links.rs b/crates/ui-egui/src/dialogs/missing_links.rs index 81a19912a5..70c2370f1b 100644 --- a/crates/ui-egui/src/dialogs/missing_links.rs +++ b/crates/ui-egui/src/dialogs/missing_links.rs @@ -8,8 +8,9 @@ //! [`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), -//! `discard` (Ignore) and `searchFolder` (set it and confirm for Find in Folder… on that folder). +//! images whose file was modified), `notes` (the open's notes, shown with the line about the +//! modified files), `applyToAll`, `path` (the replacement file; picked when empty), `discard` +//! (Ignore) and `searchFolder` (set it and confirm for Find in Folder… on that folder). use serde_json::{Value, json}; @@ -41,71 +42,94 @@ fn ids(rows: &[Value]) -> Vec { /// After a document opened (`document.open`'s result `r`): ask about its missing linked files, /// then about its modified ones, then about the fonts it opened without /// ([`super::missing_fonts`]). The web reads no linked files, so missing ones just show their -/// previews there. -pub fn after_open(app: &mut VectorcraftApp, r: &Value) { +/// previews there. The status bar shows what it says about the linked files followed by the open's +/// `notes` ([`crate::io::show_opened`]). +pub fn after_open(app: &mut VectorcraftApp, r: &Value, notes: Vec) { + after_open_for(app, r, notes, cfg!(target_arch = "wasm32")); +} + +fn after_open_for(app: &mut VectorcraftApp, r: &Value, notes: Vec, web: bool) { // The fonts' dialog waits for these questions (one dialog at a time). super::missing_fonts::after_open(app, r); let rows = |k: &str| r[k].as_array().cloned().unwrap_or_default(); let (missing, modified) = (rows("missingLinks"), ids(&rows("modifiedLinks"))); - if missing.is_empty() || cfg!(target_arch = "wasm32") { - if !missing.is_empty() { - app.status(crate::i18n::msg_n( + let notice = if missing.is_empty() || web { + let unreadable = (!missing.is_empty()).then(|| { + crate::i18n::msg_n( missing.len() as u64, "{n} linked file can't be read here: its preview shows", "{n} linked files can't be read here: their previews show", &[], - )); - } - ask_update(app, modified); + ) + }); + ask_update(app, modified).or(unreadable) } else { - open(app, missing, modified); - } + open(app, missing, modified, ¬es); + None + }; + crate::io::show_opened(app, notice, notes); super::settle(app); } -fn open(app: &mut VectorcraftApp, missing: Vec, modified: Vec) { - app.ui.dialog = Some(Dialog::new(KIND, json!({ "missing": missing, "modified": modified, "applyToAll": false, "path": "" }))); +fn open(app: &mut VectorcraftApp, missing: Vec, modified: Vec, notes: &[String]) { + let fields = json!({ "missing": missing, "modified": modified, "notes": notes, "applyToAll": false, "path": "" }); + app.ui.dialog = Some(Dialog::new(KIND, fields)); +} + +/// The open's notes a dialog `d` of the missing linked files keeps (field `notes`). +pub(super) fn notes(d: &Dialog) -> Vec { + let notes = d.fields.get("notes").and_then(Value::as_array).into_iter().flatten(); + notes.filter_map(Value::as_str).map(str::to_string).collect() } /// Ask about the missing files `rest`, one at a time, then offer the modified ones `modified` for -/// update. -pub(super) fn ask_next(app: &mut VectorcraftApp, rest: Vec, modified: Vec) { +/// update. A line about the modified files is followed by the open's `notes` +/// ([`crate::io::show_opened`]). +pub(super) fn ask_next(app: &mut VectorcraftApp, rest: Vec, modified: Vec, notes: Vec) { if rest.is_empty() { app.ui.dialog = None; - ask_update(app, modified); + if let Some(notice) = ask_update(app, modified) { + crate::io::show_opened(app, Some(notice), notes); + } } else { - open(app, rest, modified); + open(app, rest, modified, ¬es); } } -/// With Update Links: Ask When Modified, offer to read the modified linked files `ids` again. -fn ask_update(app: &mut VectorcraftApp, ids: Vec) { +/// With Update Links: Ask When Modified, offer to read the modified linked files `ids` again; +/// otherwise → the status bar's line for them ([`changed_on_disk_status`]). +fn ask_update(app: &mut VectorcraftApp, ids: Vec) -> Option { ask( app, ids, "{n} linked image changed since this document was saved. Show the new version?", "{n} linked images 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`]). #[cfg(not(target_arch = "wasm32"))] pub(crate) fn ask_update_changed(app: &mut VectorcraftApp, ids: Vec) { - ask(app, ids, "{n} linked image changed on disk. Show the new version?", "{n} linked images changed on disk. Show the new versions?"); + let status = + ask(app, ids, "{n} linked image changed on disk. Show the new version?", "{n} linked images changed on disk. Show the new versions?"); + if let Some(status) = status { + app.status(status); + } } -/// Ask about the modified linked files `ids`; `one` and `other` are the English forms of the question. -fn ask(app: &mut VectorcraftApp, ids: Vec, one: &str, other: &str) { +/// Ask about the modified linked files `ids`; `one` and `other` are the English forms of the +/// question. Without Update Links: Ask When Modified → the status bar's line for them instead. +fn ask(app: &mut VectorcraftApp, ids: Vec, one: &str, other: &str) -> Option { if ids.is_empty() { - return; + return None; } if app.session.prefs.update_links != "askWhenModified" { - app.status(changed_on_disk_status(ids.len())); - return; + return Some(changed_on_disk_status(ids.len())); } let detail = crate::i18n::tn(ids.len() as u64, one, other); super::confirm::ask(app, tl!("Update Modified Links"), &detail, "links.update", json!({ "ids": ids })); + None } /// The status bar's line for `n` linked images that changed on disk while Update Links doesn't ask. @@ -208,7 +232,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { rest.retain(|row| row["ids"].as_array().is_none_or(|ids| ids.iter().any(|id| !done.contains(id)))); } } - ask_next(app, rest, modified); + ask_next(app, rest, modified, notes(d)); Ok(out) } @@ -303,6 +327,66 @@ mod tests { assert_eq!(question("cs", 5), "Od uložení dokumentu se změnilo 5 propojených obrázků. Zobrazit nové verze?"); } + /// On the web, an open with two notes and two linked files it can't read: the status bar says + /// so about the files, then counts the notes, and keeps the notes for its hover. Without notes + /// the line about the files is shown alone. + #[test] + fn an_open_with_unreadable_links_keeps_import_notes_reachable() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + let missing = json!([{ "name": "a.png", "path": "/gone/a.png", "ids": [1] }, { "name": "b.png", "path": "/gone/b.png", "ids": [2] }]); + let notes = vec!["a note".to_string(), "another note".to_string()]; + let r = json!({ "missingLinks": missing, "warnings": notes }); + after_open_for(&mut app, &r, notes.clone(), true); + let notice = "2 linked files can't be read here: their previews show"; + assert_eq!(app.ui.status, crate::io::opened_after_notice(notice, 2, "a note")); + assert_eq!(app.ui.status_notes, Some((app.ui.status.clone(), notes))); + assert!(app.ui.dialog.is_none()); + after_open_for(&mut app, &json!({ "missingLinks": missing }), vec![], true); + assert_eq!((app.ui.status.as_str(), &app.ui.status_notes), (notice, &None)); + } + + /// The line about modified linked files that follows the Linked File Not Found dialog keeps the + /// notes of that open, also when another message was shown meanwhile. An open without notes + /// shows the line alone, even while the status bar shows an earlier open's notes, and a later + /// line about files changed on disk while the document is open doesn't take them. + #[test] + fn modified_links_after_the_missing_ones_keep_the_open_notes() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app.session.prefs.update_links = "manually".into(); + let notes = vec!["a note".to_string(), "another note".to_string()]; + let links = json!({ + "missingLinks": [{ "name": "a.png", "path": "/gone/a.png", "ids": [1] }], + "modifiedLinks": [{ "ids": [2] }], + }); + let ignore = |app: &mut VectorcraftApp| { + app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true)); + super::super::confirm(app).unwrap(); + assert!(app.ui.dialog.is_none()); + }; + after_open_for(&mut app, &links, notes.clone(), false); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(KIND)); + assert_eq!(app.ui.status, crate::io::opened_with_notes(2, "a note")); + app.status("Relinked 1 image"); + ignore(&mut app); + assert_eq!(app.ui.status, crate::io::opened_after_notice(&changed_on_disk_status(1), 2, "a note")); + assert_eq!(app.ui.status_notes, Some((app.ui.status.clone(), notes.clone()))); + + crate::io::show_opened(&mut app, None, notes); + after_open_for(&mut app, &links, vec![], false); + ignore(&mut app); + assert_eq!((app.ui.status.clone(), &app.ui.status_notes), (changed_on_disk_status(1), &None)); + + #[cfg(not(target_arch = "wasm32"))] + { + crate::io::show_opened(&mut app, None, vec!["a note".to_string(), "another note".to_string()]); + ask_update_changed(&mut app, vec![json!(2)]); + assert_eq!(app.ui.status, changed_on_disk_status(1)); + assert_ne!(app.ui.status_notes.as_ref().map(|(status, _)| status), Some(&app.ui.status)); + } + } + #[test] fn apply_to_all_replaces_the_others_from_the_same_folder() { let (mut app, dir, _) = opened_without_links("all"); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 3828d9ba2d..7a40c0bf0c 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4714,6 +4714,7 @@ axis Both И двете @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Изхвърлен е {n} възстановен документ|Изхвърлени са {n} възстановени документа @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Възстановен е {n} документ: запазете го, за да го съхраните|Възстановени са {n} документа: запазете ги, за да ги съхраните @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Отворено с {n} бележка: {first}|Отворено с {n} бележки: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Отворено с {n} бележка: {first}|{notice}. Отворено с {n} бележки: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} {path} е запазен с {n} бележка: {first}|{path} е запазен с {n} бележки: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Вградено е {n} изображение; {missing} липсват: първо ги свържете наново|Вградени са {n} изображения; {missing} липсват: първо ги свържете наново @msgplural Embedded {n} image|Embedded {n} images Вградено е {n} изображение|Вградени са {n} изображения diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 81fee3a079..58b7b6f941 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5119,6 +5119,7 @@ imageTrace Outline Obris @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Odbačen {n} oporavljeni dokument|Odbačena {n} oporavljena dokumenta|Odbačeno {n} oporavljenih dokumenata @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Vraćen {n} oporavljeni dokument; sačuvajte ga da ga zadržite|Vraćena {n} oporavljena dokumenta; sačuvajte ih da ih zadržite|Vraćeno {n} oporavljenih dokumenata; sačuvajte ih da ih zadržite @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Otvoreno s {n} bilješkom: {first}|Otvoreno s {n} bilješke: {first}|Otvoreno s {n} bilješki: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Otvoreno s {n} bilješkom: {first}|{notice}. Otvoreno s {n} bilješke: {first}|{notice}. Otvoreno s {n} bilješki: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Sačuvano {path} s {n} bilješkom: {first}|Sačuvano {path} s {n} bilješke: {first}|Sačuvano {path} s {n} bilješki: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Ugrađena je {n} slika; nedostaje {missing}: prvo ih ponovo povežite|Ugrađene su {n} slike; nedostaje {missing}: prvo ih ponovo povežite|Ugrađeno je {n} slika; nedostaje {missing}: prvo ih ponovo povežite @msgplural Embedded {n} image|Embedded {n} images Ugrađena je {n} slika|Ugrađene su {n} slike|Ugrađeno je {n} slika diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 02ae0f4a09..ae7d7f3c3e 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5870,6 +5870,7 @@ digits Malayalam Malaiàlam @msg onto must name a panel floating in another group onto ha de nomenar un panell que floti en un altre grup @msg open a document with data sets obre un document amb conjunts de dades @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} S’ha obert amb {n} nota: {first}|S’ha obert amb {n} notes: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. S’ha obert amb {n} nota: {first}|{notice}. S’ha obert amb {n} notes: {first} @msg Opened {url} Obert {url} @msg Outline Stroke: no stroked paths selected Contorneja el traç: no hi ha cap traçat amb traç seleccionat @msgplural Packaged {n} file|Packaged {n} files S’ha empaquetat {n} fitxer|S’han empaquetat {n} fitxers diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 7e3794e8a1..b86ea48841 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5308,6 +5308,7 @@ imageTrace Outline Obrys @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Zahozen {n} obnovený dokument|Zahozeny {n} obnovené dokumenty|Zahozeno {n} obnovených dokumentů @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Obnoven {n} dokument; pokud ho chcete zachovat, uložte ho|Obnoveny {n} dokumenty; pokud je chcete zachovat, uložte je|Obnoveno {n} dokumentů; pokud je chcete zachovat, uložte je @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Otevřeno, {n} poznámka: {first}|Otevřeno, {n} poznámky: {first}|Otevřeno, {n} poznámek: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Otevřeno, {n} poznámka: {first}|{notice}. Otevřeno, {n} poznámky: {first}|{notice}. Otevřeno, {n} poznámek: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Uloženo: {path}, {n} poznámka: {first}|Uloženo: {path}, {n} poznámky: {first}|Uloženo: {path}, {n} poznámek: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Vložen {n} obrázek; chybí {missing}: nejprve je znovu propojte|Vloženy {n} obrázky; chybí {missing}: nejprve je znovu propojte|Vloženo {n} obrázků; chybí {missing}: nejprve je znovu propojte @msgplural Embedded {n} image|Embedded {n} images Vložen {n} obrázek|Vloženy {n} obrázky|Vloženo {n} obrázků diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index f7011e23bf..db148d9936 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -4804,6 +4804,7 @@ imageTrace Outline Kontur @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Kasserede {n} gendannet dokument|Kasserede {n} gendannede dokumenter @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Gendannede {n} dokument: gem det for at beholde det|Gendannede {n} dokumenter: gem dem for at beholde dem @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Åbnede med {n} note: {first}|Åbnede med {n} noter: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Åbnede med {n} note: {first}|{notice}. Åbnede med {n} noter: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Gemte {path} med {n} note: {first}|Gemte {path} med {n} noter: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Indlejrede {n} billede; {missing} mangler: link dem igen først|Indlejrede {n} billeder; {missing} mangler: link dem igen først @msgplural Embedded {n} image|Embedded {n} images Indlejrede {n} billede|Indlejrede {n} billeder diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 4174537f87..4633461315 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4925,6 +4925,7 @@ theme Appearance Erscheinungsbild @msgplural Discarded {n} recovered document|Discarded {n} recovered documents {n} wiederhergestelltes Dokument verworfen|{n} wiederhergestellte Dokumente verworfen @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them {n} wiederhergestelltes Dokument geöffnet: Speichern Sie es, um es zu behalten|{n} wiederhergestellte Dokumente geöffnet: Speichern Sie sie, um sie zu behalten @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Mit {n} Hinweis geöffnet: {first}|Mit {n} Hinweisen geöffnet: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Mit {n} Hinweis geöffnet: {first}|{notice}. Mit {n} Hinweisen geöffnet: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} {path} mit {n} Hinweis gespeichert: {first}|{path} mit {n} Hinweisen gespeichert: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first {n} Bild eingebettet; {missing} fehlen: Verknüpfen Sie sie zuerst neu|{n} Bilder eingebettet; {missing} fehlen: Verknüpfen Sie sie zuerst neu @msgplural Embedded {n} image|Embedded {n} images {n} Bild eingebettet|{n} Bilder eingebettet diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index bde2fbee78..138a7427ab 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4947,6 +4947,7 @@ digits Malayalam Μαλαγιάλαμ @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Απορρίφθηκε {n} ανακτημένο έγγραφο|Απορρίφθηκαν {n} ανακτημένα έγγραφα @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Επαναφέρθηκε {n} ανακτημένο έγγραφο: αποθηκεύστε το για να διατηρηθεί|Επαναφέρθηκαν {n} ανακτημένα έγγραφα: αποθηκεύστε τα για να διατηρηθούν @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Άνοιξε με {n} σημείωση: {first}|Άνοιξε με {n} σημειώσεις: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Άνοιξε με {n} σημείωση: {first}|{notice}. Άνοιξε με {n} σημειώσεις: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Αποθηκεύτηκε το {path} με {n} σημείωση: {first}|Αποθηκεύτηκε το {path} με {n} σημειώσεις: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Ενσωματώθηκε {n} εικόνα· λείπουν {missing}: επανασυνδέστε τις πρώτα|Ενσωματώθηκαν {n} εικόνες· λείπουν {missing}: επανασυνδέστε τις πρώτα @msgplural Embedded {n} image|Embedded {n} images Ενσωματώθηκε {n} εικόνα|Ενσωματώθηκαν {n} εικόνες diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index ff28d9dfc5..65c6fb15c2 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5000,6 +5000,7 @@ axis Both Ambos @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Se descartó {n} documento recuperado|Se descartaron {n} documentos recuperados @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Se restauró {n} documento recuperado: guárdalo para conservarlo|Se restauraron {n} documentos recuperados: guárdalos para conservarlos @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Se abrió con {n} nota: {first}|Se abrió con {n} notas: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Se abrió con {n} nota: {first}|{notice}. Se abrió con {n} notas: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Se guardó {path} con {n} nota: {first}|Se guardó {path} con {n} notas: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Se incrustó {n} imagen; faltan {missing}: vuelve a vincularlas primero|Se incrustaron {n} imágenes; faltan {missing}: vuelve a vincularlas primero @msgplural Embedded {n} image|Embedded {n} images Se incrustó {n} imagen|Se incrustaron {n} imágenes diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index bb74188ed8..82273e72dc 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -5208,6 +5208,7 @@ imageTrace Outline Ääriviiva @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Hylättiin {n} palautettu asiakirja|Hylättiin {n} palautettua asiakirjaa @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Palautettiin {n} asiakirja: tallenna se säilyttääksesi sen|Palautettiin {n} asiakirjaa: tallenna ne säilyttääksesi ne @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Avattiin, {n} huomautus: {first}|Avattiin, {n} huomautusta: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Avattiin, {n} huomautus: {first}|{notice}. Avattiin, {n} huomautusta: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Tallennettiin {path}, {n} huomautus: {first}|Tallennettiin {path}, {n} huomautusta: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Upotettiin {n} kuva; {missing} puuttuu: linkitä ne ensin uudelleen|Upotettiin {n} kuvaa; {missing} puuttuu: linkitä ne ensin uudelleen @msgplural Embedded {n} image|Embedded {n} images Upotettiin {n} kuva|Upotettiin {n} kuvaa diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 09c9c35b0d..f56186f809 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4987,6 +4987,7 @@ axis Both Les deux @msgplural Discarded {n} recovered document|Discarded {n} recovered documents {n} document récupéré supprimé|{n} documents récupérés supprimés @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them {n} document récupéré restauré : enregistrez-le pour le conserver|{n} documents récupérés restaurés : enregistrez-les pour les conserver @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Ouvert avec {n} remarque : {first}|Ouvert avec {n} remarques : {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Ouvert avec {n} remarque : {first}|{notice}. Ouvert avec {n} remarques : {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} {path} enregistré avec {n} remarque : {first}|{path} enregistré avec {n} remarques : {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first {n} image incorporée ; il en manque {missing} : rétablissez d’abord leurs liens|{n} images incorporées ; il en manque {missing} : rétablissez d’abord leurs liens @msgplural Embedded {n} image|Embedded {n} images {n} image incorporée|{n} images incorporées diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 9aa4bd7b99..d485893eab 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -8482,6 +8482,7 @@ antiAlias Sharp शार्प @msg the document sets a bleed that this PDF/X file leaves out: turn on Use document bleed settings डॉक्यूमेंट एक ब्लीड सेट करता है जिसे यह PDF/X फ़ाइल छोड़ देती है: "डॉक्यूमेंट की ब्लीड सेटिंग्स उपयोग करें" चालू करें @msgplural Exported {what} with {n} note: {first}|Exported {what} with {n} notes: {first} {what} एक्सपोर्ट हुआ, {n} नोट के साथ: {first}|{what} एक्सपोर्ट हुआ, {n} नोट्स के साथ: {first} @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} {n} नोट के साथ खुला: {first}|{n} नोट्स के साथ खुला: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}। {n} नोट के साथ खुला: {first}|{notice}। {n} नोट्स के साथ खुला: {first} @msgplural Packaged {n} file ({note}) in {folder}|Packaged {n} files ({note}) in {folder} {folder} में {n} फ़ाइल ({note}) पैकेज हुई|{folder} में {n} फ़ाइलें ({note}) पैकेज हुईं @msgplural Packaged {n} file {note}|Packaged {n} files {note} {n} फ़ाइल पैकेज हुई {note}|{n} फ़ाइलें पैकेज हुईं {note} @msgplural Packaged {n} file in {folder}|Packaged {n} files in {folder} {folder} में {n} फ़ाइल पैकेज हुई|{folder} में {n} फ़ाइलें पैकेज हुईं diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index af6f586880..752448c650 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5441,6 +5441,7 @@ imageTrace Outline Körvonal @msg onto must name a floating panel az onto egy lebegő panelt kell megnevezzen @msg onto must name a panel floating in another group az onto egy másik csoportban lebegő panelt kell megnevezzen @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Megnyitva; megjegyzések: {n}, az első: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Megnyitva; megjegyzések: {n}, az első: {first} @msg Opened {url} Megnyitva: {url} @msg Outline Stroke: no stroked paths selected Körvonal útvonallá alakítása: nincs kijelölt körvonalas útvonal @msgplural Packaged {n} file ({note}) in {folder}|Packaged {n} files ({note}) in {folder} Becsomagolt fájlok: {n}, mappa: {folder} ({note}) diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index e0ba31ecd5..b1db30906d 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -4804,6 +4804,7 @@ imageTrace Outline Kontur @msgplural Discarded {n} recovered document|Discarded {n} recovered documents {n} dokumen hasil pemulihan dibuang @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them {n} dokumen hasil pemulihan dipulihkan: simpan agar tidak hilang @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Dibuka dengan {n} catatan: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Dibuka dengan {n} catatan: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} {path} disimpan dengan {n} catatan: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first {n} gambar tersemat; {missing} hilang: tautkan ulang dulu @msgplural Embedded {n} image|Embedded {n} images {n} gambar tersemat diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index ce0bc58ce5..f1530d76a2 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5051,6 +5051,7 @@ axis Both Entrambi @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Scartato {n} documento recuperato|Scartati {n} documenti recuperati @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Ripristinato {n} documento recuperato. Salvalo per conservarlo|Ripristinati {n} documenti recuperati. Salvali per conservarli @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Aperto con {n} nota: {first}|Aperto con {n} note: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Aperto con {n} nota: {first}|{notice}. Aperto con {n} note: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Salvato {path} con {n} nota: {first}|Salvato {path} con {n} note: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Incorporata {n} immagine; mancanti: {missing} (ricollegale prima)|Incorporate {n} immagini; mancanti: {missing} (ricollegale prima) @msgplural Embedded {n} image|Embedded {n} images Incorporata {n} immagine|Incorporate {n} immagini diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 1ecce4618a..396a991018 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5168,6 +5168,7 @@ swatch Bright Violet ビビッドバイオレット @msgplural Discarded {n} recovered document|Discarded {n} recovered documents 復帰用データのドキュメントを {n} 件破棄しました @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them 復帰用データからドキュメントを {n} 件復元しました: 残すには保存してください @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} ドキュメントを開きました (注意 {n} 件): {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}。ドキュメントを開きました (注意 {n} 件): {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} {path} を保存しましたが、注意事項が {n} 件あります: {first} @msg Saving recovery data for {_1} {_1} の復帰用データを保存中 @msg This shortcut file is in a form VectorCraft can't read. このショートカットファイルは VectorCraft で読み込めない形式です。 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 3d6425c373..d5b190cc95 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4653,6 +4653,7 @@ font-technology Variable ცვლადი @msgplural Discarded {n} recovered document|Discarded {n} recovered documents უარყოფილია აღდგენილი დოკუმენტები: {n} @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them აღდგენილია დოკუმენტები: {n}: შესანარჩუნებლად შეინახეთ ისინი @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} გაიხსნა, შენიშვნები: {n}: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. გაიხსნა, შენიშვნები: {n}: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} შენახულია {path}, შენიშვნები: {n}: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first ჩაშენებულია გამოსახულებები: {n}; აკლია {missing}: ჯერ ხელახლა მიაბით @msgplural Embedded {n} image|Embedded {n} images ჩაშენებულია გამოსახულებები: {n} diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index fb87488d64..4700516cbb 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5095,6 +5095,7 @@ theme Appearance 외관 @msgplural Discarded {n} recovered document|Discarded {n} recovered documents 복구된 문서 {n}개를 삭제했습니다 @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them 복구된 문서 {n}개를 복원했습니다: 보관하려면 저장하세요 @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} 참고 {n}건과 함께 열었습니다: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. 참고 {n}건과 함께 열었습니다: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} 참고 {n}건과 함께 {path}을(를) 저장했습니다: {first} @msg Saving recovery data for {_1} {_1}의 복구 데이터를 저장하는 중 @msg bad shortcut 잘못된 단축키 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index b3388fa82a..e56e1dafb1 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -4816,6 +4816,7 @@ imageTrace Outline Contour @msgplural Discarded {n} recovered document|Discarded {n} recovered documents {n} hersteld document verworpen|{n} herstelde documenten verworpen @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them {n} hersteld document teruggezet: sla het op om het te behouden|{n} herstelde documenten teruggezet: sla ze op om ze te behouden @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Geopend met {n} opmerking: {first}|Geopend met {n} opmerkingen: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Geopend met {n} opmerking: {first}|{notice}. Geopend met {n} opmerkingen: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} {path} opgeslagen met {n} opmerking: {first}|{path} opgeslagen met {n} opmerkingen: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first {n} afbeelding ingesloten; {missing} ontbreken: koppel ze eerst opnieuw|{n} afbeeldingen ingesloten; {missing} ontbreken: koppel ze eerst opnieuw @msgplural Embedded {n} image|Embedded {n} images {n} afbeelding ingesloten|{n} afbeeldingen ingesloten diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 0729e35c79..a0bf6e3590 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5134,6 +5134,7 @@ imageTrace Outline Kontur @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Odrzucono {n} odzyskany dokument|Odrzucono {n} odzyskane dokumenty|Odrzucono {n} odzyskanych dokumentów @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Przywrócono {n} odzyskany dokument: zapisz go, aby go zachować|Przywrócono {n} odzyskane dokumenty: zapisz je, aby je zachować|Przywrócono {n} odzyskanych dokumentów: zapisz je, aby je zachować @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Otwarto z {n} uwagą: {first}|Otwarto z {n} uwagami: {first}|Otwarto z {n} uwagami: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Otwarto z {n} uwagą: {first}|{notice}. Otwarto z {n} uwagami: {first}|{notice}. Otwarto z {n} uwagami: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Zapisano {path} z {n} uwagą: {first}|Zapisano {path} z {n} uwagami: {first}|Zapisano {path} z {n} uwagami: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Osadzono {n} obraz; brakuje {missing}: najpierw połącz je ponownie|Osadzono {n} obrazy; brakuje {missing}: najpierw połącz je ponownie|Osadzono {n} obrazów; brakuje {missing}: najpierw połącz je ponownie @msgplural Embedded {n} image|Embedded {n} images Osadzono {n} obraz|Osadzono {n} obrazy|Osadzono {n} obrazów diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 36f778dbd5..96fdc810e5 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4455,6 +4455,7 @@ axis Both Ambos @msgplural Discarded {n} recovered document|Discarded {n} recovered documents {n} documento recuperado descartado|{n} documentos recuperados descartados @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them {n} documento recuperado restaurado: salve-o para mantê-lo|{n} documentos recuperados restaurados: salve-os para mantê-los @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Aberto com {n} observação: {first}|Aberto com {n} observações: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Aberto com {n} observação: {first}|{notice}. Aberto com {n} observações: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} {path} salvo com {n} observação: {first}|{path} salvo com {n} observações: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first {n} imagem incorporada; faltam {missing}: revincule-as primeiro|{n} imagens incorporadas; faltam {missing}: revincule-as primeiro @msgplural Embedded {n} image|Embedded {n} images {n} imagem incorporada|{n} imagens incorporadas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 442a9612d4..dc6eac5f77 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5037,6 +5037,7 @@ axis Both Оба @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Отменён {n} восстановленный документ|Отменено {n} восстановленных документа|Отменено {n} восстановленных документов @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Восстановлен {n} документ: сохраните его, чтобы оставить|Восстановлено {n} документа: сохраните их, чтобы оставить|Восстановлено {n} документов: сохраните их, чтобы оставить @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Открыто с {n} примечанием: {first}|Открыто с {n} примечаниями: {first}|Открыто с {n} примечаниями: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Открыто с {n} примечанием: {first}|{notice}. Открыто с {n} примечаниями: {first}|{notice}. Открыто с {n} примечаниями: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Сохранено: {path} с {n} примечанием: {first}|Сохранено: {path} с {n} примечаниями: {first}|Сохранено: {path} с {n} примечаниями: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Внедрено {n} изображение; отсутствует {missing}: сначала восстановите связи|Внедрено {n} изображения; отсутствует {missing}: сначала восстановите связи|Внедрено {n} изображений; отсутствует {missing}: сначала восстановите связи @msgplural Embedded {n} image|Embedded {n} images Внедрено {n} изображение|Внедрено {n} изображения|Внедрено {n} изображений diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 8a8f8453b6..f3a1ec5566 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4895,6 +4895,7 @@ axis Both Båda @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Kastade {n} återställt dokument|Kastade {n} återställda dokument @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Återställde {n} återskapat dokument: spara det för att behålla det|Återställde {n} återskapade dokument: spara dem för att behålla dem @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Öppnad med {n} anteckning: {first}|Öppnad med {n} anteckningar: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Öppnad med {n} anteckning: {first}|{notice}. Öppnad med {n} anteckningar: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Sparade {path} med {n} anteckning: {first}|Sparade {path} med {n} anteckningar: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Bäddade in {n} bild; {missing} saknas: länka om dem först|Bäddade in {n} bilder; {missing} saknas: länka om dem först @msgplural Embedded {n} image|Embedded {n} images Bäddade in {n} bild|Bäddade in {n} bilder diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index 66caa1c3a4..68160ced9d 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -2194,6 +2194,10 @@ const OPEN_SAVE_COUNTED_MESSAGES: &[(&str, &str)] = &[ ("Saved /out/a.pdf with 1 note: a note", "Saved /out/a.pdf with 2 notes: a note"), ("Discarded 1 recovered document", "Discarded 2 recovered documents"), ("Restored 1 recovered document: save it to keep it", "Restored 2 recovered documents: save them to keep them"), + ( + "1 linked image changed on disk: Update Links shows the new version. Opened with 1 note: a note", + "1 linked image changed on disk: Update Links shows the new version. Opened with 2 notes: a note", + ), ]; /// The open, save and recovery status messages and the Global Edit options' Results line name a @@ -2201,15 +2205,18 @@ const OPEN_SAVE_COUNTED_MESSAGES: &[(&str, &str)] = &[ /// translates both, in the form its plural rule picks. #[test] fn open_save_and_recovery_messages_take_the_plural_form() { - use crate::io::{opened_with_notes, saved_status}; + use crate::dialogs::missing_links::changed_on_disk_status; + use crate::io::{opened_after_notice, opened_with_notes, saved_status}; use crate::panels::properties::global_edit_results; use crate::recovery::recovered_status; let saved = |path: &str, n: usize| saved_status(path, &serde_json::json!({ "warnings": vec!["a note"; n] })); + let notice = changed_on_disk_status(1); let made = [ (opened_with_notes(1, "a note"), opened_with_notes(2, "a note")), (saved("/out/a.pdf", 1), saved("/out/a.pdf", 2)), (recovered_status(true, 1), recovered_status(true, 2)), (recovered_status(false, 1), recovered_status(false, 2)), + (opened_after_notice(¬ice, 1, "a note"), opened_after_notice(¬ice, 2, "a note")), ]; for ((one, other), (want_one, want_other)) in made.iter().zip(OPEN_SAVE_COUNTED_MESSAGES) { assert_eq!((one.as_str(), other.as_str()), (*want_one, *want_other)); @@ -2221,7 +2228,18 @@ fn open_save_and_recovery_messages_take_the_plural_form() { for s in OPEN_SAVE_COUNTED_MESSAGES.iter().flat_map(|(one, other)| [one, other]) { assert_ne!(message(lang, s), *s, "{code}: {s}"); } + // The line about linked files and the open's line are each shown as the catalog has them. + for n in [1, 2] { + let shown = message(lang, &opened_after_notice(¬ice, n, "a note")); + let (link, open) = (message(lang, ¬ice), message(lang, &opened_with_notes(n, "a note"))); + assert!(link != notice && shown.starts_with(&link) && shown.ends_with(&open), "{code}: {shown}"); + } } + let unreadable = "2 linked files can't be read here: their previews show"; + assert_eq!( + message(de(), &opened_after_notice(unreadable, 3, "a note")), + format!("{}. Mit 3 Hinweisen geöffnet: a note", message(de(), unreadable)) + ); for l in LANGUAGES.iter().filter(|l| l.complete_menus) { let lang = Lang::from_code(l.code).expect("registered"); for n in [1, 2] { diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index ef8773c31b..db16ff59cc 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4733,6 +4733,7 @@ axis Both Her İkisi @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Kurtarılan {n} belge atıldı @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Kurtarılan {n} belge geri yüklendi: saklamak için kaydedin @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} {n} notla açıldı: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. {n} notla açıldı: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} {path}, {n} notla kaydedildi: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first {n} görsel gömüldü; {missing} eksik: önce onları yeniden bağlayın @msgplural Embedded {n} image|Embedded {n} images {n} görsel gömüldü diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 0bccbd8cb6..4af518f8a5 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5035,6 +5035,7 @@ axis Both Обидві @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Скасовано {n} відновлений документ|Скасовано {n} відновлені документи|Скасовано {n} відновлених документів @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them Відновлено {n} документ: збережіть його, щоб залишити|Відновлено {n} документи: збережіть їх, щоб залишити|Відновлено {n} документів: збережіть їх, щоб залишити @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Відкрито з {n} приміткою: {first}|Відкрито з {n} примітками: {first}|Відкрито з {n} примітками: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Відкрито з {n} приміткою: {first}|{notice}. Відкрито з {n} примітками: {first}|{notice}. Відкрито з {n} примітками: {first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} Збережено: {path} з {n} приміткою: {first}|Збережено: {path} з {n} примітками: {first}|Збережено: {path} з {n} примітками: {first} @msgplural Embedded {n} image; {missing} missing: relink them first|Embedded {n} images; {missing} missing: relink them first Вбудовано {n} зображення; відсутніх: {missing}. Спочатку відновіть їхні зв’язки|Вбудовано {n} зображення; відсутніх: {missing}. Спочатку відновіть їхні зв’язки|Вбудовано {n} зображень; відсутніх: {missing}. Спочатку відновіть їхні зв’язки @msgplural Embedded {n} image|Embedded {n} images Вбудовано {n} зображення|Вбудовано {n} зображення|Вбудовано {n} зображень diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 4f084029db..d88dfe4f5d 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5104,6 +5104,7 @@ theme Appearance Giao diện @msg onto must name a floating panel onto phải đặt tên bảng đang thả nổi @msg onto must name a panel floating in another group onto phải đặt tên bảng đang thả nổi trong nhóm khác @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} Đã mở với {n} ghi chú: {first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}. Đã mở với {n} ghi chú: {first} @msg Opened {url} Đã mở {url} @msg Outline Stroke: no stroked paths selected Đường bao nét: chưa chọn đường dẫn có nét @msgplural Packaged {n} file ({note}) in {folder}|Packaged {n} files ({note}) in {folder} Đã đóng gói {n} tệp trong {folder} ({note}) diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index b04e65a65b..d2b48b3676 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4856,6 +4856,7 @@ imageTrace Outline 轮廓 @msgplural Discarded {n} recovered document|Discarded {n} recovered documents 已丢弃 {n} 个恢复的文档 @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them 已还原 {n} 个恢复的文档:请保存以保留它们 @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} 已打开,有 {n} 条注释:{first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}。已打开,有 {n} 条注释:{first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} 已保存 {path},有 {n} 条注释:{first} @msg Saving recovery data for {_1} 正在保存 {_1} 的恢复数据 @msg This shortcut file is in a form VectorCraft can't read. 此快捷键文件的格式 VectorCraft 无法读取。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index be62fde8a7..c7d74275f4 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2623,6 +2623,7 @@ file-filter Templates 範本 @msgplural Discarded {n} recovered document|Discarded {n} recovered documents 已捨棄 {n} 份復原的文件 @msgplural Restored {n} recovered document: save it to keep it|Restored {n} recovered documents: save them to keep them 已還原 {n} 份復原的文件:請儲存以保留 @msgplural Opened with {n} note: {first}|Opened with {n} notes: {first} 已開啟,附有 {n} 則註解:{first} +@msgplural {notice}. Opened with {n} note: {first}|{notice}. Opened with {n} notes: {first} {notice}。已開啟,附有 {n} 則註解:{first} @msgplural Saved {path} with {n} note: {first}|Saved {path} with {n} notes: {first} 已儲存 {path},附有 {n} 則註解:{first} @msg Saving recovery data for {_1} 正在儲存 {_1} 的復原資料 @msg This shortcut file is in a form VectorCraft can't read. VectorCraft 無法讀取此快速鍵檔案的格式。 diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index 5f70c5dc63..83f463d668 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -26,13 +26,13 @@ pub(crate) use export::{ export_files, exported_with_notes, file_url, open_in_app, open_original, reveal_path, run_to_file, save_command_output_named, }; pub(crate) use open::report_installed; -pub(crate) use open::{EXPORT_PRESETS, import_presets, open_with_on, opened}; +pub(crate) use open::{EXPORT_PRESETS, import_presets, open_with_on, opened, show_opened}; pub use open::{ MAX_RECENT_FILES, OpensAs, ask_revert, install_module, install_plugin, new_from_template, note_recent, open_bytes, open_dialog, open_document, open_path, open_path_with, open_reporting, open_with, opens_as, place_bytes, recent_files, report_open_error, }; #[cfg(test)] -pub(crate) use open::{open_file, opened_with_notes}; +pub(crate) use open::{open_file, opened_after_notice, opened_with_notes}; pub(crate) use save::save_current; pub use save::{ask_format_options, ask_pdf_options, remember_active_artboard, remember_view, save, save_pdf}; pub(crate) use save::{export_path, is_web, notes_of, report_saved, show_with_notes, target_path, write_named, write_reporting, write_to}; @@ -87,9 +87,8 @@ mod tests { /// A file opened with a note about its page first and a note that its layers weren't read /// second: the status bar names the layers note, counts both, and keeps both, the layers note - /// first, for its hover. A message about its linked files still replaces that message. Two - /// notes of what was left out keep the order the import gave them, and an open without notes - /// leaves the message and the notes an earlier open left. + /// first, for its hover. Two notes of what was left out keep the order the import gave them, + /// and an open without notes leaves the message and the notes an earlier open left. #[test] fn an_open_names_the_note_of_what_was_left_out_first() { use vectorcraft_testkit::ai; @@ -103,14 +102,6 @@ mod tests { assert_eq!(app.ui.status, opened_with_notes(2, layers)); assert_eq!(app.ui.status_notes, Some((app.ui.status.clone(), vec![layers.clone(), page.clone()]))); - // A message about its linked files replaces the open's message, as before. - app.session.prefs.update_links = "manually".into(); - let mut linked = r.clone(); - linked["modifiedLinks"] = json!([{ "ids": [1] }]); - opened(&mut app, &linked); - assert_eq!(app.ui.status, dialogs::missing_links::changed_on_disk_status(1)); - assert_eq!(app.ui.status_notes, Some((opened_with_notes(2, layers), vec![layers.clone(), page.clone()]))); - let r = open_bytes(&mut app, "damaged.ai", &ai::ai(damaged, ai::page_pdf()), None).unwrap(); let both = notes(&r); assert_eq!(both.len(), 2, "{r}"); @@ -130,6 +121,38 @@ mod tests { assert_eq!((app.ui.status.clone(), app.ui.status_notes.clone()), shown); } + /// A file opened with two notes and a modified linked file while Update Links doesn't ask: the + /// status bar shows the line about the linked file, then counts the notes and names the first + /// of what was left out, and keeps both notes, in that order, for its hover. + #[test] + fn an_open_with_modified_links_keeps_link_notice_and_import_notes_reachable() { + let (mut app, _) = app(); + let pdf = vectorcraft_testkit::ai::pdf(b"%AI12_CompressedData not zlib", "0 g 150 20 100 60 re f"); + let r = open_bytes(&mut app, "damaged.pdf", &pdf, None).unwrap(); + let [page, layers] = ¬es_of(&r)[..] else { panic!("two notes: {r}") }; + app.session.prefs.update_links = "manually".into(); + let mut linked = r.clone(); + linked["modifiedLinks"] = json!([{ "ids": [1] }]); + opened(&mut app, &linked); + let notice = dialogs::missing_links::changed_on_disk_status(1); + assert_eq!(app.ui.status, opened_after_notice(¬ice, 2, layers)); + assert!(app.ui.status.starts_with(¬ice) && app.ui.status.ends_with(&opened_with_notes(2, layers)), "{}", app.ui.status); + assert_eq!(app.ui.status_notes, Some((app.ui.status.clone(), vec![layers.clone(), page.clone()]))); + assert!(app.ui.dialog.is_none()); + } + + /// File › New from Template… shows the notes of the file it opens, as File › Open does. + #[test] + fn new_from_template_preserves_open_warnings() { + let (mut app, _) = app(); + let pdf = vectorcraft_testkit::ai::pdf(b"%AI12_CompressedData not zlib", "0 g 150 20 100 60 re f"); + app.services.read = Some(Box::new(move |_: &str| Ok(pdf.clone()))); + let r = new_from_template(&mut app, Some("/templates/damaged.pdf".into())).unwrap(); + let [page, layers] = ¬es_of(&r)[..] else { panic!("two notes: {r}") }; + assert_eq!(app.ui.status, opened_with_notes(2, layers)); + assert_eq!(app.ui.status_notes, Some((app.ui.status.clone(), vec![layers.clone(), page.clone()]))); + } + /// A save and an export with several notes: the status bar names the first, counts them, and /// keeps every one of them, in the reply's order, for its hover. A save without notes keeps /// none. diff --git a/crates/ui-egui/src/io/open.rs b/crates/ui-egui/src/io/open.rs index ae52b599b7..1dec3a1fc7 100644 --- a/crates/ui-egui/src/io/open.rs +++ b/crates/ui-egui/src/io/open.rs @@ -140,14 +140,24 @@ pub fn open_document(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: O /// what didn't come in as it was, and missing links are asked about. The status bar names the /// first of the notes that say reading the file left something out (the ones that make Save ask /// before writing over it), else the first note, and keeps the notes in that order for its hover. +/// A line about the document's linked files comes before the notes ([`show_opened`]). pub(crate) fn opened(app: &mut VectorcraftApp, r: &Value) { app.sync_views(); let notes = open_notes(app, r); - if let Some(first) = notes.first() { - let status = opened_with_notes(notes.len(), first); - show_with_notes(app, status, notes); - } - crate::dialogs::missing_links::after_open(app, r); + crate::dialogs::missing_links::after_open(app, r, notes); +} + +/// Show what an open says in the status bar: `notice`, a line about the document's linked files, +/// then the open's `notes` ([`opened_with_notes`]), kept for the message's hover. Without either +/// the status bar keeps its message. +pub(crate) fn show_opened(app: &mut VectorcraftApp, notice: Option, notes: Vec) { + let status = match (notice, notes.first()) { + (Some(notice), Some(first)) => opened_after_notice(¬ice, notes.len(), first), + (None, Some(first)) => opened_with_notes(notes.len(), first), + (Some(notice), None) => notice, + (None, None) => return, + }; + show_with_notes(app, status, notes); } /// The notes of the open `r`, those of what reading the file left out (its document's @@ -166,6 +176,16 @@ pub(crate) fn opened_with_notes(n: usize, first: &str) -> String { crate::i18n::msg_n(n as u64, "Opened with {n} note: {first}", "Opened with {n} notes: {first}", &[("first", first)]) } +/// [`opened_with_notes`] after `notice`, a line about the document's linked files. +pub(crate) fn opened_after_notice(notice: &str, n: usize, first: &str) -> String { + crate::i18n::msg_n( + n as u64, + "{notice}. Opened with {n} note: {first}", + "{notice}. Opened with {n} notes: {first}", + &[("notice", notice), ("first", first)], + ) +} + /// A path from the open dialog, or "cancelled". fn pick_open(app: &mut VectorcraftApp, pick: &FilePick) -> Result { crate::picks::open(app, pick).ok_or_else(|| "cancelled".into()) @@ -564,8 +584,7 @@ pub fn new_from_template(app: &mut VectorcraftApp, path: Option) -> Resu }; let bytes = read(app, &path)?; let r = fileio::open_template(&mut app.session, &path, &bytes, Some(&path)).map_err(|e| e.to_string())?; - app.sync_views(); - crate::dialogs::missing_links::after_open(app, &r); + opened(app, &r); Ok(r) } diff --git a/crates/ui-egui/src/recovery.rs b/crates/ui-egui/src/recovery.rs index f6188babca..053960dbb7 100644 --- a/crates/ui-egui/src/recovery.rs +++ b/crates/ui-egui/src/recovery.rs @@ -133,7 +133,7 @@ pub fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { match app.run(id, json!({ "file": file })) { Ok(r) => { if let Some(restored) = r["restored"].as_array().and_then(|v| v.first()) { - crate::dialogs::missing_links::after_open(app, restored); + crate::dialogs::missing_links::after_open(app, restored, Vec::new()); } done.push(r); } diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 5694d9582d..f694587897 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -107,7 +107,7 @@ the two can still win that race. | `ui.render` | `{path?, scale?, artboard?, data?, traceViews?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first; `traceViews: true`: Image Trace objects draw in their View, as on the canvas, instead of their tracing result): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` | | `ui.render` | `{path?, scale?, artboard?, data?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` | | `ui.resize` / `ui.focus` | | | -| `app.open` / `app.save` / `app.export` / `app.quit` | `{path, importer?, params?}` / `{path?}` / `{path?, format?, artboard?, range?, scale?, …}` | `app.open` reads every format `document.open` reads (see `document.formats`) and returns its result (`{index, title, format, warnings, …}`; `warnings` say what didn't come in as it was, such as an EPS shown as its preview image and why its PostScript couldn't be read), or null when a dialog asks first or a library loads. The status bar (`ui.inspect` → `ui.status`) counts the `warnings` and names the first that says reading the file left something out (the notes that make Save ask before writing over it), else the first; with several, hovering it lists them in that order, as many as fit in the window (at most 30), then how many more. After `app.save`, and `app.export` with a `path`, the status bar counts the reply's `warnings` and names the first, and with several, hovering it lists them in the reply's order (a save's notes start with what the format loses). With `importer` (an importer plug-in's format id, `plugin.`) it runs `file.openWith`: the file opens with that importer, also when a built-in format reads its extension, with the importer's parameters `params` (their defaults when omitted; no dialog asks for them). Without `importer`, `params` go to the importer that opens the file, and a built-in format does not read them. `app.export` encodes through the engine's `document.export` (same options; the document keeps its path) and writes `path` through the host; without `path` it returns `{dataBase64, format, bytes}`, as headless mode does. It refuses a key that no format takes, and an unknown key inside a PDF or EPS option object, before it writes anything (`docs/mcp.md`, Parameter keys). `app.save` refuses a key that no save format takes, beside the path or in `options`, the same way. `app.quit`, `file.close` and `file.closeAll` first open a `saveChanges` dialog for each modified document (they return `{"pending": "saveChanges"}`): `ui.dialog.confirm` saves, `ui.dialog.set {field: "discard", value: true}` then confirm discards, `ui.dialog.cancel` cancels the whole close or quit | +| `app.open` / `app.save` / `app.export` / `app.quit` | `{path, importer?, params?}` / `{path?}` / `{path?, format?, artboard?, range?, scale?, …}` | `app.open` reads every format `document.open` reads (see `document.formats`) and returns its result (`{index, title, format, warnings, …}`; `warnings` say what didn't come in as it was, such as an EPS shown as its preview image and why its PostScript couldn't be read), or null when a dialog asks first or a library loads. The status bar (`ui.inspect` → `ui.status`), after a line about the document's linked files when there is one (Missing linked files below), counts the `warnings` and names the first that says reading the file left something out (the notes that make Save ask before writing over it), else the first; with several, hovering it lists them in that order, as many as fit in the window (at most 30), then how many more. After `app.save`, and `app.export` with a `path`, the status bar counts the reply's `warnings` and names the first, and with several, hovering it lists them in the reply's order (a save's notes start with what the format loses). With `importer` (an importer plug-in's format id, `plugin.`) it runs `file.openWith`: the file opens with that importer, also when a built-in format reads its extension, with the importer's parameters `params` (their defaults when omitted; no dialog asks for them). Without `importer`, `params` go to the importer that opens the file, and a built-in format does not read them. `app.export` encodes through the engine's `document.export` (same options; the document keeps its path) and writes `path` through the host; without `path` it returns `{dataBase64, format, bytes}`, as headless mode does. It refuses a key that no format takes, and an unknown key inside a PDF or EPS option object, before it writes anything (`docs/mcp.md`, Parameter keys). `app.save` refuses a key that no save format takes, beside the path or in `options`, the same way. `app.quit`, `file.close` and `file.closeAll` first open a `saveChanges` dialog for each modified document (they return `{"pending": "saveChanges"}`): `ui.dialog.confirm` saves, `ui.dialog.set {field: "discard", value: true}` then confirm discards, `ui.dialog.cancel` cancels the whole close or quit | | `app.export` with `useArtboards` | `{path, useArtboards: true, range? \| artboards?, …}` | Export As: PNG, JPEG and WebP write one file per chosen artboard (default all; SVG does so for a `range`), `-.`, and return `{path, files: [path…]}`; a PDF keeps them as pages of `path`. `useArtboards: false` covers the bounds of the visible art. The menu's Export As… is `file.exportAs`: the `exportAs` dialog (`format`, `useArtboards`, `all`, `range`), then a save dialog and the format's options: `pngOptions` / `jpgOptions` / `webpOptions`, `svgOptions`, or `savePdf` for a PDF | | `ui.dialog.*` on `gradientStop` | `{field: "color" \| "opacity" \| "location", value}` | double-clicking a stop on the Gradient tool's annotator opens its popover (fields `index`, `x`, `y`, `tab`); set `color` (hex), `opacity` or `location` (percentages) and `ui.dialog.confirm` to apply them to the selected stop. `gradient.selectStop {index}` picks the stop the annotator, the panels and Delete/←/→ (`ui.key`) act on | | `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). `useArtboards: false` opens it for one page of the visible art's bounds, as Export As with Use Artboards off does: the dialog has no Artboards section and its `__artBounds` field is true. `ui.dialog.confirm` writes `path` (else asks for one), returns `{path, bytes, warnings}` and opens the file when `viewAfterSaving` is set. With `useArtboards: false` it writes the page as `app.export` does and returns `{path, warnings}`, or `{background: true, status}` while Background Export writes it | @@ -645,7 +645,7 @@ agents) opens the `rasterEffectsSettings` dialog. Its fields are what `document. Missing linked files: `app.open` of a document whose linked images' files can't be found (`document.open`'s `missingLinks`) opens the `missingLinks` dialog for the first (fields `missing`: `[{name, path, ids}]`, `modified`, -`applyToAll`, `path`). `ui.dialog.set {field: "path", value}` then `ui.dialog.confirm` replaces it with that file +`notes` (the open's `warnings`), `applyToAll`, `path`). `ui.dialog.set {field: "path", value}` then `ui.dialog.confirm` replaces it with that file (`links.relink`; without `path` a file is picked), and with `applyToAll` the others are looked for by name in the same folder; `ui.dialog.set {field: "discard", value: true}` then confirm ignores it (the images keep their preview), with `applyToAll` the rest too. The next missing file is asked about after each answer; `ui.dialog.cancel` stops asking. @@ -653,11 +653,14 @@ Then, with the preference `updateLinks: "askWhenModified"`, modified linked file `confirm` dialog whose `ui.dialog.confirm` runs `links.update`. The same happens when the app's window comes back to the front and a linked file of the active document changed meanwhile (asked once per change of a file); with `"automatically"` the files are read again at once, with `"manually"` the Links panel shows them as modified. +After an open, the status bar's line about modified files (with `"manually"`) or, on the web, about files that can't +be read comes first, followed by the line that counts the open's `warnings`; hovering it lists them as for any open. +When the line about modified files follows the `missingLinks` dialog, it is followed by that dialog's `notes`. Find Linked Files: in the `missingLinks` dialog, `ui.dialog.set {field: "searchFolder", value}` then `ui.dialog.confirm` is Find in Folder…: the dialog becomes `findLinks`, which looks in that folder and its subfolders for every file the dialog still asks about (`links.findFiles`). Fields: `document` (the document's uid), `missing` (`[{name, path, ids}]`), -`modified`, `back`, `state` (empty, `searching`, `done`, `stopped` or `failed`), `search` (the search's `id`), `folder`, +`modified`, `notes` (from `missingLinks`), `back`, `state` (empty, `searching`, `done`, `stopped` or `failed`), `search` (the search's `id`), `folder`, `found` (`links.findFiles`'s `links`, with the files found), `searched`, `skipped`, `unreadable`, `stopped`, `error`, `picks` (the file chosen for each row of `missing`, or null; with each search result, a row not chosen in the dialog gets its only same file, else its only file found), `touched` (the rows whose file was chosen in the dialog; search diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f04ee3c910..9dad520384 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1652,6 +1652,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - On Windows, a document opened from a mapped network drive finds its links stored with the share's own path (`\\server\share\…`), as the same document opened from the share does; links on other computers stay missing. - On Windows, a link on a drive mapped to a network share other than the document's (`Y:\…`) is missing, with the reason that the path is on another computer, as a link stored with that share's own path is; links on this computer's drives and on the document's share are followed. - In the Links panel, Shift-click selects a range of links and Ctrl/Cmd-click adds or removes one. + - When a document opens with notes and with linked files that changed on disk (Update Links: Manually) or, on the web, can't be read, the status bar shows the line about the linked files, then counts the notes and names the first, and hovering it lists the notes. The same holds when the line about modified files follows the Linked File Not Found dialog, and File › New from Template… shows the notes of the file it opens as File › Open does (`an_open_with_modified_links_keeps_link_notice_and_import_notes_reachable`, `new_from_template_preserves_open_warnings`, `an_open_with_unreadable_links_keeps_import_notes_reachable`, `modified_links_after_the_missing_ones_keep_the_open_notes`, `combined_open_and_link_status_lists_notes_on_hover_and_for_accessibility`). - 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`). - A look at the linked files whose worker thread ends without a result, such as after a panic, no longer stops later looks: the app clears it, stops asking for a frame every 100 ms, and the next look starts on schedule (`crates/engine/src/tests/linkwatch.rs`, `crates/ui-egui/src/tests/place.rs`). - Import and paste, 2026-10-13: a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542). Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479). Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499).