From e802ea8438910216b813c248e453c139e39ddd78 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 14 Oct 2026 08:44:42 +0700 Subject: [PATCH] Fix #1685: a save or export that fails is reported in a dialog that stays A save that couldn't write its file (a read-only folder, a file another program holds open, a full disk) was only reported in the status bar, a line the next message replaces; a background save's failure arrived there some time after the user had moved on. Save, Save As, Save a Copy, Save as Template, Export As and the commands that write one file now also say it in a dialog naming the file and the reason, which stays until it is closed. The document stays modified and keeps the file it had. A message that arrives while another dialog is open waits for it to close instead of replacing it. An agent's request gets the error as its reply, without a dialog; a failed recovery write stays in the status bar. --- crates/ui-egui/src/dialogs/confirm.rs | 21 +++++- crates/ui-egui/src/dialogs/mod.rs | 1 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/io.rs | 94 +++++++++++++++++++++++++-- crates/ui-egui/src/lib.rs | 5 +- crates/ui-egui/src/panels/links.rs | 4 +- crates/ui-egui/src/state.rs | 10 +++ 21 files changed, 154 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/dialogs/confirm.rs b/crates/ui-egui/src/dialogs/confirm.rs index ba166874..555a1b83 100644 --- a/crates/ui-egui/src/dialogs/confirm.rs +++ b/crates/ui-egui/src/dialogs/confirm.rs @@ -41,10 +41,27 @@ pub const MESSAGE: &str = "message"; pub(super) const MESSAGE_SPEC: DialogSpec = DialogSpec { ok: None, ..SPEC }; +/// Most messages kept for later while a dialog is open ([`tell`]). +const MAX_WAITING: usize = 16; + /// Tell the user `message` (with an optional `detail` line), both in the UI language, in a dialog -/// they close. +/// they close. While another dialog is open (the message comes from work in the background) it +/// waits its turn instead of replacing it: neither what the user was doing there nor the message +/// is lost. pub fn tell(app: &mut VectorcraftApp, message: &str, detail: &str) { - app.ui.dialog = Some(Dialog::new(MESSAGE, json!({"message": message, "detail": detail}))); + let d = Dialog::new(MESSAGE, json!({"message": message, "detail": detail})); + if app.ui.dialog.is_none() { + app.ui.dialog = Some(d); + } else if app.ui.told.len() < MAX_WAITING && !app.ui.dialog.iter().chain(&app.ui.told).any(|t| t.kind == MESSAGE && t.fields == d.fields) { + app.ui.told.push(d); + } +} + +/// Show the next message that waited for a dialog to close (each frame). +pub(super) fn show_waiting(app: &mut VectorcraftApp) { + if app.ui.dialog.is_none() && !app.ui.told.is_empty() { + app.ui.dialog = Some(app.ui.told.remove(0)); + } } /// Closes before running, so a dialog the command opens stays open. diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index 40d1ca19..5deb182a 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -366,6 +366,7 @@ pub(crate) fn settle(app: &mut VectorcraftApp) { pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { about::show(app, ctx); settle(app); + confirm::show_waiting(app); // The kind of dialog shown last frame: a different one (or none) means this one just opened, and // its first field is to take the keyboard focus (`focus_id`, until a field takes it). let (shown_id, focus_id) = (egui::Id::new("dialog-shown"), egui::Id::new("dialog-focus-pending")); diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 4b35d262..db12bd3a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3508,6 +3508,8 @@ effect Regular Pravidelný Serif / Mincho Patkové / mincho Changes made since it was last saved will be lost. Změny provedené od posledního uložení budou ztraceny. Can't open “{name}”. „{name}“ nelze otevřít. + Can't save “{name}”. „{name}“ nelze uložit. + Can't export “{name}”. „{name}“ nelze exportovat. Downloaded {what} Staženo: {what} Exported {count} file(s) to {folder} Exportováno souborů: {count} do {folder} Exported {what} Exportováno: {what} diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 51b06bad..cf7bb1be 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -754,6 +754,8 @@ Do Not Move Nicht verschieben Do you want to save the changes you made to “{name}”? Möchten Sie die Änderungen an „{name}“ speichern? Can't open “{name}”. „{name}“ kann nicht geöffnet werden. + Can't save “{name}”. „{name}“ kann nicht gespeichert werden. + Can't export “{name}”. „{name}“ kann nicht exportiert werden. Document Dokument Document Color Mode Dokumentfarbmodus Document Info Dokumentinformationen diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index c9dba867..b04fc1f2 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -768,6 +768,8 @@ Do Not Move No mover Do you want to save the changes you made to “{name}”? ¿Quieres guardar los cambios hechos en “{name}”? Can't open “{name}”. No se puede abrir «{name}». + Can't save “{name}”. No se puede guardar «{name}». + Can't export “{name}”. No se puede exportar «{name}». Document Documento Document Color Mode Modo de color del documento Document Info Información del documento diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index cccd38e7..4176af3a 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -770,6 +770,8 @@ Do Not Move Ne pas déplacer Do you want to save the changes you made to “{name}”? Voulez-vous enregistrer les modifications apportées à « {name} » ? Can't open “{name}”. Impossible d’ouvrir « {name} ». + Can't save “{name}”. Impossible d’enregistrer « {name} ». + Can't export “{name}”. Impossible d’exporter « {name} ». Document Document Document Color Mode Mode colorimétrique du document Document Info Informations sur le document diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 89d39aae..ec2e69fd 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -485,6 +485,8 @@ Calligraphic Kalligrafikus Calligraphic Brush Options Kalligrafikus ecset beállításai Can't open “{name}”. Nem nyitható meg: „{name}”. + Can't save “{name}”. Nem menthető: „{name}”. + Can't export “{name}”. Nem exportálható: „{name}”. Can't print these settings: see the Summary Ezekkel a beállításokkal nem lehet nyomtatni: lásd az Összegzést Cancel Mégse Cancel Key Object Kulcsobjektum megszüntetése diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 494252d0..b414c815 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -768,6 +768,8 @@ Do Not Move Non spostare Do you want to save the changes you made to “{name}”? Vuoi salvare le modifiche apportate a “{name}”? Can't open “{name}”. Impossibile aprire «{name}». + Can't save “{name}”. Impossibile salvare «{name}». + Can't export “{name}”. Impossibile esportare «{name}». Document Documento Document Color Mode Metodo colore documento Document Info Informazioni documento diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index edb50b54..90b7b3c6 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1223,6 +1223,8 @@ Divide Vertically Into 垂直方向に分割 Do you want to save the changes you made to “{name}”? 「{name}」への変更を保存しますか? Can't open “{name}”. 「{name}」を開けません。 + Can't save “{name}”. 「{name}」を保存できません。 + Can't export “{name}”. 「{name}」を書き出せません。 Document ドキュメント Document Ink Options ドキュメントのインキオプション Document JSON ドキュメント JSON diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 14070de7..02b1d58b 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1218,6 +1218,8 @@ Divide Vertically Into 세로로 나누기 Do you want to save the changes you made to “{name}”? “{name}”의 변경 내용을 저장하시겠습니까? Can't open “{name}”. “{name}”을(를) 열 수 없습니다. + Can't save “{name}”. “{name}”을(를) 저장할 수 없습니다. + Can't export “{name}”. “{name}”을(를) 내보낼 수 없습니다. Document 문서 Document Ink Options 문서 잉크 옵션 Document JSON 문서 JSON diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 869e2a9b..c98e48a5 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3391,6 +3391,8 @@ effect Regular Normalny Serif / Mincho Szeryfowe / mincho Changes made since it was last saved will be lost. Zmiany wprowadzone od ostatniego zapisu zostaną utracone. Can't open “{name}”. Nie można otworzyć „{name}”. + Can't save “{name}”. Nie można zapisać „{name}”. + Can't export “{name}”. Nie można wyeksportować „{name}”. Downloaded {what} Pobrano {what} Exported {count} file(s) to {folder} Wyeksportowane pliki do {folder}: {count} Exported {what} Wyeksportowano {what} diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index e5b3e964..c39b61c7 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -692,6 +692,8 @@ Divide Vertically Into Dividir Verticalmente Em Do you want to save the changes you made to “{name}”? Deseja salvar as alterações feitas em “{name}”? Can't open “{name}”. Não é possível abrir “{name}”. + Can't save “{name}”. Não é possível salvar “{name}”. + Can't export “{name}”. Não é possível exportar “{name}”. Document Documento Document Color Mode Modo de Cor do Documento Document Info Informações do Documento diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 1b92d00d..e4a9a39f 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -766,6 +766,8 @@ Do Not Move Не перемещать Do you want to save the changes you made to “{name}”? Сохранить изменения, внесённые в «{name}»? Can't open “{name}”. Не удаётся открыть «{name}». + Can't save “{name}”. Не удаётся сохранить «{name}». + Can't export “{name}”. Не удаётся экспортировать «{name}». Document Документ Document Color Mode Цветовой режим документа Document Info Сведения о документе diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 91999762..268d1933 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -762,6 +762,8 @@ Do Not Move Не переміщати Do you want to save the changes you made to “{name}”? Зберегти зміни, внесені до «{name}»? Can't open “{name}”. Не вдається відкрити «{name}». + Can't save “{name}”. Не вдається зберегти «{name}». + Can't export “{name}”. Не вдається експортувати «{name}». Document Документ Document Color Mode Колірний режим документа Document Info Відомості про документ diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 44dfae0a..63cf26f6 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -401,6 +401,8 @@ Calligraphic Thư pháp Calligraphic Brush Options Tùy chọn cọ thư pháp Can't open “{name}”. Không thể mở “{name}”. + Can't save “{name}”. Không thể lưu “{name}”. + Can't export “{name}”. Không thể xuất “{name}”. Can't print these settings: see the Summary Không thể in các cài đặt này: xem Tóm tắt Cancel Hủy Cancel Key Object Hủy đối tượng chính diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 0ed1c633..6ac0c6a7 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -723,6 +723,8 @@ Do Not Move 不移动 Do you want to save the changes you made to “{name}”? 要存储对“{name}”所做的更改吗? Can't open “{name}”. 无法打开“{name}”。 + Can't save “{name}”. 无法保存“{name}”。 + Can't export “{name}”. 无法导出“{name}”。 Document 文档 Document Color Mode 文档颜色模式 Document Info 文档信息 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 70e2d1a0..6b769b8a 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -741,6 +741,8 @@ Do Not Move 不移動 Do you want to save the changes you made to “{name}”? 要儲存對「{name}」所做的變更嗎? Can't open “{name}”. 無法開啟「{name}」。 + Can't save “{name}”. 無法儲存「{name}」。 + Can't export “{name}”. 無法匯出「{name}」。 Document 文件 Document Color Mode 文件色彩模式 Document Info 文件資訊 diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index 66198372..d5dd5b5d 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -167,6 +167,29 @@ pub fn report_open_error(app: &mut VectorcraftApp, name: &str, e: &str) { dialogs::confirm::tell(app, &message, &crate::i18n::message(crate::i18n::current(), e)); } +/// A save or an export that didn't write `path`: say so in a dialog that names the file and the +/// reason and stays until it is closed, as an editor does (#1685). The status bar's line, which +/// the caller's error becomes, is replaced by the next message, and a document that wasn't saved +/// must not look saved: it stays modified, and what was at `path` is as it was. Not for a request +/// an agent sent and waits on: the error is its reply. +pub(crate) fn report_write_error(app: &mut VectorcraftApp, export: bool, path: &str, e: &str) { + if app.ui.answering_control { + return; + } + let name = fileio::file_name(path); + let message = if export { tl!("Can't export “{name}”.") } else { tl!("Can't save “{name}”.") }; + dialogs::confirm::tell(app, &crate::i18n::fmt(message, &[("name", &name)]), &crate::i18n::message(crate::i18n::current(), e)); +} + +/// [`write_to`], a failure reported ([`report_write_error`]). +pub(crate) fn write_reporting(app: &mut VectorcraftApp, path: &str, bytes: &[u8]) -> Result<(), String> { + let r = write_to(&mut app.services, path, bytes); + if let Err(e) = &r { + report_write_error(app, false, path, e); + } + r +} + /// [`open_path`] for a file the user asked for, a failure reported ([`report_open_error`]). On /// the desktop a document is decoded off the UI thread ([`crate::opening`]): the result is then /// `{background: true, status}`, and the document opens in its tab (or the failure is reported) @@ -259,7 +282,7 @@ pub(crate) fn write_named(app: &mut VectorcraftApp, path: Option, name: pick_path(app, &FilePick { name: name.to_string(), folder, ..Default::default() })? } }; - write_to(&mut app.services, &path, bytes)?; + write_reporting(app, &path, bytes)?; Ok(path) } @@ -515,8 +538,10 @@ fn write_plan(app: &mut VectorcraftApp, plan: SavePlan) -> Result } .map_err(|e| e.to_string()) }; + let failed = target.clone().unwrap_or_default(); let then = move |app: &mut VectorcraftApp, r: Result| { - let r = r?; + // Not written: the document keeps its file, format and modified state, and the user is told. + let r = r.inspect_err(|e| report_write_error(app, false, &failed, e))?; // The document may have closed meanwhile: the file is written all the same. done.complete(&mut app.session, uid); let path = r["path"].as_str().unwrap_or_default().to_string(); @@ -601,8 +626,9 @@ pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option format!("{} files", files.len()), None => r["path"].as_str().unwrap_or_default().to_string(), @@ -672,7 +698,7 @@ pub(crate) fn run_to_file(app: &mut VectorcraftApp, id: &str, ext: &str, mut par Some(p) => p, None => target_path(app, None, ext)?, }; - write_to(&mut app.services, &path, &bytes)?; + write_reporting(app, &path, &bytes)?; Ok((path, v)) } @@ -829,13 +855,69 @@ mod tests { assert!(app.ui.dialog.is_none()); } + /// #1685: a save or an export that can't write its file says so in a dialog that names the + /// file and the reason and stays until it is closed; the document stays modified and keeps + /// the file it had. An agent's request only gets the error, and a message that arrives while + /// another dialog is open waits for it to close. + #[test] + fn a_save_that_cannot_write_is_reported_in_a_dialog_that_stays() { + let refuse: crate::WriteFn = Box::new(|p: &str, _: &[u8]| Err(format!("{p}: Access is denied. (os error 5)"))); + let mut app = VectorcraftApp::new(Session::new(), Services { write: Some(refuse), ..Default::default() }); + app.run("file.new", json!({"width": 100, "height": 80})).unwrap(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap(); + let unsaved = |app: &VectorcraftApp| { + let st = app.session.doc().unwrap(); + (st.is_dirty(), st.path.clone()) + }; + for (command, path, message) in [ + ("file.save", "/docs/Poster.vectorcraft", "Can't save “Poster.vectorcraft”."), + ("file.saveAs", "/docs/Poster.vectorcraft", "Can't save “Poster.vectorcraft”."), + ("file.saveCopy", "/docs/Copy.vectorcraft", "Can't save “Copy.vectorcraft”."), + ("file.exportAs", "/docs/Poster.png", "Can't export “Poster.png”."), + ("file.export.pdf", "/docs/Poster.pdf", "Can't save “Poster.pdf”."), + ] { + let e = app.run(command, json!({"path": path})).unwrap_err(); + assert!(e.contains("Access is denied"), "{command}: {e}"); + let d = app.ui.dialog.take().unwrap_or_else(|| panic!("{command}: a dialog says so")); + assert_eq!((d.kind.as_str(), d.str("message").as_str()), (dialogs::confirm::MESSAGE, message), "{command}"); + assert_eq!(d.str("detail"), format!("{path}: Access is denied. (os error 5)"), "{command}"); + assert_eq!(unsaved(&app), (true, None), "{command}"); + } + // An agent waiting on its request gets the error as its reply, and no dialog. + app.ui.answering_control = true; + assert!(app.run("file.save", json!({"path": "/docs/Poster.vectorcraft"})).is_err()); + app.ui.answering_control = false; + assert!(app.ui.dialog.is_none() && app.ui.told.is_empty()); + // Under another dialog (a background save that fails later) the message waits its turn, + // once, and is drawn when that dialog is closed. + dialogs::confirm::ask(&mut app, "Delete the swatch?", "", "edit.undo", json!({})); + for _ in 0..2 { + report_write_error(&mut app, false, "/docs/Poster.vectorcraft", "the disk is full"); + } + assert_eq!((app.ui.dialog.as_ref().map(|d| d.kind.as_str()), app.ui.told.len()), (Some(dialogs::confirm::KIND), 1)); + app.ui.dialog = None; + for _ in 0..2 { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| dialogs::show(&mut app, ui.ctx())); + out.textures_delta.clear(); + } + let d = app.ui.dialog.as_ref().expect("the message is shown, and stays"); + assert_eq!( + (d.kind.as_str(), d.str("message").as_str(), d.str("detail").as_str()), + (dialogs::confirm::MESSAGE, "Can't save “Poster.vectorcraft”.", "the disk is full") + ); + assert!(app.ui.told.is_empty()); + } + /// #1693: a browser download that can't be started is a failed save: the document stays /// modified and without a file, and Save Changes doesn't close it. #[test] fn a_download_that_cannot_start_leaves_the_document_unsaved_and_open() { let started = Rc::new(RefCell::new(false)); let ok = started.clone(); - let download: crate::DownloadFn = Box::new(move |_: &str, _: &[u8]| if *ok.borrow() { Ok(()) } else { Err("the browser refused the download".into()) }); + let download: crate::DownloadFn = + Box::new(move |_: &str, _: &[u8]| if *ok.borrow() { Ok(()) } else { Err("the browser refused the download".into()) }); let mut app = VectorcraftApp::new(Session::new(), Services { download: Some(download), ..Default::default() }); app.run("file.new", json!({"width": 100, "height": 80})).unwrap(); app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index aae24fbe..012decbf 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -780,7 +780,10 @@ impl VectorcraftApp { let reply = req.reply.clone(); // Guarded per request: a panic must not drop the channel (taken out of `self` above). let roots = self.automation_roots.clone(); - let outcome = match file_access::confine(roots.as_ref(), || vectorcraft_engine::guard::catch_panic(|| control::handle(self, ctx, &req))) { + self.ui.answering_control = true; + let answer = file_access::confine(roots.as_ref(), || vectorcraft_engine::guard::catch_panic(|| control::handle(self, ctx, &req))); + self.ui.answering_control = false; + let outcome = match answer { Ok(outcome) => outcome, Err(msg) => { self.picks.recover(); diff --git a/crates/ui-egui/src/panels/links.rs b/crates/ui-egui/src/panels/links.rs index 6031e6ef..329d9d88 100644 --- a/crates/ui-egui/src/panels/links.rs +++ b/crates/ui-egui/src/panels/links.rs @@ -158,9 +158,9 @@ pub(crate) fn unembed(app: &mut VectorcraftApp, id: u64, name: &str) { if app.services.download.is_some() { if let Ok(v) = app.session.execute("links.unembed", &json!({ "id": id })) && let (Some(name), Some(bytes)) = (v["name"].as_str(), v["dataBase64"].as_str().and_then(vectorcraft_format::base64_decode)) - && let Err(e) = crate::io::write_to(&mut app.services, name, &bytes) { - app.status(e); + // A download that can't start is said like any file that can't be written. + let _ = crate::io::write_reporting(app, name, &bytes); } return; } diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index e4eb10a0..1fc6f089 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -306,6 +306,14 @@ pub struct UiState { pub dock: bool, pub view: ViewFlags, pub dialog: Option, + /// Messages to show in a dialog once the open one is closed (`dialogs::confirm::tell`), in + /// the order they came. + #[serde(skip)] + pub told: Vec, + /// A control request is being answered: its caller gets what fails as its reply, so no + /// dialog says it too (`io::report_write_error`). + #[serde(skip)] + pub answering_control: bool, /// Toolbar group whose flyout is open. pub flyout: Option, /// Last tool shown for each toolbar group (flyout selection sticks). @@ -681,6 +689,8 @@ impl Default for UiState { dock: true, view: ViewFlags::default(), dialog: None, + told: vec![], + answering_control: false, flyout: None, group_tool: vectorcraft_tools::TOOL_GROUPS.iter().map(|g| g[0].id.to_string()).collect(), status: String::new(),