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.
This commit is contained in:
bflatastic 2026-10-14 08:44:42 +07:00
parent 8f8d49110f
commit e802ea8438
21 changed files with 154 additions and 11 deletions

View File

@ -41,10 +41,27 @@ pub const MESSAGE: &str = "message";
pub(super) const MESSAGE_SPEC: DialogSpec = DialogSpec { ok: None, ..SPEC }; 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 /// 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) { 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. /// Closes before running, so a dialog the command opens stays open.

View File

@ -366,6 +366,7 @@ pub(crate) fn settle(app: &mut VectorcraftApp) {
pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
about::show(app, ctx); about::show(app, ctx);
settle(app); settle(app);
confirm::show_waiting(app);
// The kind of dialog shown last frame: a different one (or none) means this one just opened, and // 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). // 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")); let (shown_id, focus_id) = (egui::Id::new("dialog-shown"), egui::Id::new("dialog-focus-pending"));

View File

@ -3508,6 +3508,8 @@ effect Regular Pravidelný
Serif / Mincho Patkové / mincho Serif / Mincho Patkové / mincho
Changes made since it was last saved will be lost. Změny provedené od posledního uložení budou ztraceny. 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 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} Downloaded {what} Staženo: {what}
Exported {count} file(s) to {folder} Exportováno souborů: {count} do {folder} Exported {count} file(s) to {folder} Exportováno souborů: {count} do {folder}
Exported {what} Exportováno: {what} Exported {what} Exportováno: {what}

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -754,6 +754,8 @@
Do Not Move Nicht verschieben Do Not Move Nicht verschieben
Do you want to save the changes you made to “{name}”? Möchten Sie die Änderungen an „{name}“ speichern? 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 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 Dokument
Document Color Mode Dokumentfarbmodus Document Color Mode Dokumentfarbmodus
Document Info Dokumentinformationen Document Info Dokumentinformationen

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -768,6 +768,8 @@
Do Not Move No mover Do Not Move No mover
Do you want to save the changes you made to “{name}”? ¿Quieres guardar los cambios hechos en “{name}”? 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 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 Documento
Document Color Mode Modo de color del documento Document Color Mode Modo de color del documento
Document Info Información del documento Document Info Información del documento

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -770,6 +770,8 @@
Do Not Move Ne pas déplacer 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} » ? 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 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 Document
Document Color Mode Mode colorimétrique du document Document Color Mode Mode colorimétrique du document
Document Info Informations sur le document Document Info Informations sur le document

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -485,6 +485,8 @@
Calligraphic Kalligrafikus Calligraphic Kalligrafikus
Calligraphic Brush Options Kalligrafikus ecset beállításai Calligraphic Brush Options Kalligrafikus ecset beállításai
Can't open “{name}”. Nem nyitható meg: „{name}”. 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 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 Mégse
Cancel Key Object Kulcsobjektum megszüntetése Cancel Key Object Kulcsobjektum megszüntetése

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -768,6 +768,8 @@
Do Not Move Non spostare Do Not Move Non spostare
Do you want to save the changes you made to “{name}”? Vuoi salvare le modifiche apportate a “{name}”? 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 open “{name}”. Impossibile aprire «{name}».
Can't save “{name}”. Impossibile salvare «{name}».
Can't export “{name}”. Impossibile esportare «{name}».
Document Documento Document Documento
Document Color Mode Metodo colore documento Document Color Mode Metodo colore documento
Document Info Informazioni documento Document Info Informazioni documento

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -1223,6 +1223,8 @@
Divide Vertically Into 垂直方向に分割 Divide Vertically Into 垂直方向に分割
Do you want to save the changes you made to “{name}”? 「{name}」への変更を保存しますか? Do you want to save the changes you made to “{name}”? 「{name}」への変更を保存しますか?
Can't open “{name}”. 「{name}」を開けません。 Can't open “{name}”. 「{name}」を開けません。
Can't save “{name}”. 「{name}」を保存できません。
Can't export “{name}”. 「{name}」を書き出せません。
Document ドキュメント Document ドキュメント
Document Ink Options ドキュメントのインキオプション Document Ink Options ドキュメントのインキオプション
Document JSON ドキュメント JSON Document JSON ドキュメント JSON

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -1218,6 +1218,8 @@
Divide Vertically Into 세로로 나누기 Divide Vertically Into 세로로 나누기
Do you want to save the changes you made to “{name}”? “{name}”의 변경 내용을 저장하시겠습니까? Do you want to save the changes you made to “{name}”? “{name}”의 변경 내용을 저장하시겠습니까?
Can't open “{name}”. “{name}”을(를) 열 수 없습니다. Can't open “{name}”. “{name}”을(를) 열 수 없습니다.
Can't save “{name}”. “{name}”을(를) 저장할 수 없습니다.
Can't export “{name}”. “{name}”을(를) 내보낼 수 없습니다.
Document 문서 Document 문서
Document Ink Options 문서 잉크 옵션 Document Ink Options 문서 잉크 옵션
Document JSON 문서 JSON Document JSON 문서 JSON

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3391,6 +3391,8 @@ effect Regular Normalny
Serif / Mincho Szeryfowe / mincho Serif / Mincho Szeryfowe / mincho
Changes made since it was last saved will be lost. Zmiany wprowadzone od ostatniego zapisu zostaną utracone. 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 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} Downloaded {what} Pobrano {what}
Exported {count} file(s) to {folder} Wyeksportowane pliki do {folder}: {count} Exported {count} file(s) to {folder} Wyeksportowane pliki do {folder}: {count}
Exported {what} Wyeksportowano {what} Exported {what} Wyeksportowano {what}

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -692,6 +692,8 @@
Divide Vertically Into Dividir Verticalmente Em 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}”? 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 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 Documento
Document Color Mode Modo de Cor do Documento Document Color Mode Modo de Cor do Documento
Document Info Informações do Documento Document Info Informações do Documento

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -766,6 +766,8 @@
Do Not Move Не перемещать Do Not Move Не перемещать
Do you want to save the changes you made to “{name}”? Сохранить изменения, внесённые в «{name}»? Do you want to save the changes you made to “{name}”? Сохранить изменения, внесённые в «{name}»?
Can't open “{name}”. Не удаётся открыть «{name}». Can't open “{name}”. Не удаётся открыть «{name}».
Can't save “{name}”. Не удаётся сохранить «{name}».
Can't export “{name}”. Не удаётся экспортировать «{name}».
Document Документ Document Документ
Document Color Mode Цветовой режим документа Document Color Mode Цветовой режим документа
Document Info Сведения о документе Document Info Сведения о документе

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -762,6 +762,8 @@
Do Not Move Не переміщати Do Not Move Не переміщати
Do you want to save the changes you made to “{name}”? Зберегти зміни, внесені до «{name}»? Do you want to save the changes you made to “{name}”? Зберегти зміни, внесені до «{name}»?
Can't open “{name}”. Не вдається відкрити «{name}». Can't open “{name}”. Не вдається відкрити «{name}».
Can't save “{name}”. Не вдається зберегти «{name}».
Can't export “{name}”. Не вдається експортувати «{name}».
Document Документ Document Документ
Document Color Mode Колірний режим документа Document Color Mode Колірний режим документа
Document Info Відомості про документ Document Info Відомості про документ

Can't render this file because it has a wrong number of fields in line 13.

View File

@ -401,6 +401,8 @@
Calligraphic Thư pháp Calligraphic Thư pháp
Calligraphic Brush Options Tùy chọn cọ 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 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 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 Hủy
Cancel Key Object Hủy đối tượng chính Cancel Key Object Hủy đối tượng chính

Can't render this file because it has a wrong number of fields in line 11.

View File

@ -723,6 +723,8 @@
Do Not Move 不移动 Do Not Move 不移动
Do you want to save the changes you made to “{name}”? 要存储对“{name}”所做的更改吗? Do you want to save the changes you made to “{name}”? 要存储对“{name}”所做的更改吗?
Can't open “{name}”. 无法打开“{name}”。 Can't open “{name}”. 无法打开“{name}”。
Can't save “{name}”. 无法保存“{name}”。
Can't export “{name}”. 无法导出“{name}”。
Document 文档 Document 文档
Document Color Mode 文档颜色模式 Document Color Mode 文档颜色模式
Document Info 文档信息 Document Info 文档信息

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -741,6 +741,8 @@
Do Not Move 不移動 Do Not Move 不移動
Do you want to save the changes you made to “{name}”? 要儲存對「{name}」所做的變更嗎? Do you want to save the changes you made to “{name}”? 要儲存對「{name}」所做的變更嗎?
Can't open “{name}”. 無法開啟「{name}」。 Can't open “{name}”. 無法開啟「{name}」。
Can't save “{name}”. 無法儲存「{name}」。
Can't export “{name}”. 無法匯出「{name}」。
Document 文件 Document 文件
Document Color Mode 文件色彩模式 Document Color Mode 文件色彩模式
Document Info 文件資訊 Document Info 文件資訊

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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)); 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 /// [`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 /// 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) /// `{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<String>, name:
pick_path(app, &FilePick { name: name.to_string(), folder, ..Default::default() })? 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) Ok(path)
} }
@ -515,8 +538,10 @@ fn write_plan(app: &mut VectorcraftApp, plan: SavePlan) -> Result<Value, String>
} }
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
}; };
let failed = target.clone().unwrap_or_default();
let then = move |app: &mut VectorcraftApp, r: Result<Value, String>| { let then = move |app: &mut VectorcraftApp, r: Result<Value, String>| {
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. // The document may have closed meanwhile: the file is written all the same.
done.complete(&mut app.session, uid); done.complete(&mut app.session, uid);
let path = r["path"].as_str().unwrap_or_default().to_string(); 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<Strin
Ok(out) Ok(out)
}; };
let enabled = app.session.prefs.background_export; let enabled = app.session.prefs.background_export;
let mut r = background::run(app, enabled, label, None, work, |app, r| { let failed = target.clone();
let r = r?; let mut r = background::run(app, enabled, label, None, work, move |app, r| {
let r = r.inspect_err(|e| report_write_error(app, true, &failed, e))?;
let what = match r["files"].as_array() { let what = match r["files"].as_array() {
Some(files) => format!("{} files", files.len()), Some(files) => format!("{} files", files.len()),
None => r["path"].as_str().unwrap_or_default().to_string(), 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, Some(p) => p,
None => target_path(app, None, ext)?, None => target_path(app, None, ext)?,
}; };
write_to(&mut app.services, &path, &bytes)?; write_reporting(app, &path, &bytes)?;
Ok((path, v)) Ok((path, v))
} }
@ -829,13 +855,69 @@ mod tests {
assert!(app.ui.dialog.is_none()); 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 /// #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. /// modified and without a file, and Save Changes doesn't close it.
#[test] #[test]
fn a_download_that_cannot_start_leaves_the_document_unsaved_and_open() { fn a_download_that_cannot_start_leaves_the_document_unsaved_and_open() {
let started = Rc::new(RefCell::new(false)); let started = Rc::new(RefCell::new(false));
let ok = started.clone(); 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() }); 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("file.new", json!({"width": 100, "height": 80})).unwrap();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap(); app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap();

View File

@ -780,7 +780,10 @@ impl VectorcraftApp {
let reply = req.reply.clone(); let reply = req.reply.clone();
// Guarded per request: a panic must not drop the channel (taken out of `self` above). // Guarded per request: a panic must not drop the channel (taken out of `self` above).
let roots = self.automation_roots.clone(); 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, Ok(outcome) => outcome,
Err(msg) => { Err(msg) => {
self.picks.recover(); self.picks.recover();

View File

@ -158,9 +158,9 @@ pub(crate) fn unembed(app: &mut VectorcraftApp, id: u64, name: &str) {
if app.services.download.is_some() { if app.services.download.is_some() {
if let Ok(v) = app.session.execute("links.unembed", &json!({ "id": id })) 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 (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; return;
} }

View File

@ -306,6 +306,14 @@ pub struct UiState {
pub dock: bool, pub dock: bool,
pub view: ViewFlags, pub view: ViewFlags,
pub dialog: Option<Dialog>, pub dialog: Option<Dialog>,
/// 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<Dialog>,
/// 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. /// Toolbar group whose flyout is open.
pub flyout: Option<usize>, pub flyout: Option<usize>,
/// Last tool shown for each toolbar group (flyout selection sticks). /// Last tool shown for each toolbar group (flyout selection sticks).
@ -681,6 +689,8 @@ impl Default for UiState {
dock: true, dock: true,
view: ViewFlags::default(), view: ViewFlags::default(),
dialog: None, dialog: None,
told: vec![],
answering_control: false,
flyout: None, flyout: None,
group_tool: vectorcraft_tools::TOOL_GROUPS.iter().map(|g| g[0].id.to_string()).collect(), group_tool: vectorcraft_tools::TOOL_GROUPS.iter().map(|g| g[0].id.to_string()).collect(),
status: String::new(), status: String::new(),