Merge pull request #1865 from storytold/fix/1685-save-failure-dialog
Fix #1693, #1694, #1685: a failed save is reported in a dialog that stays; failed downloads; opens under a dialog
This commit is contained in:
commit
ecf86ca21e
|
|
@ -434,11 +434,8 @@ fn services(inbox: Inbox, place_inbox: PlaceInbox, ctx: egui::Context, locks: Op
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
});
|
});
|
||||||
})),
|
})),
|
||||||
download: Some(Box::new(|name: &str, bytes: &[u8]| {
|
// A download that couldn't be started fails the save or export it is for (#1693).
|
||||||
if let Err(e) = download(name, bytes) {
|
download: Some(Box::new(|name: &str, bytes: &[u8]| download(name, bytes).inspect_err(|e| log::error!("download of {name} failed: {e}")))),
|
||||||
log::error!("download of {name} failed: {e}");
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
inbox: Some(inbox),
|
inbox: Some(inbox),
|
||||||
recovery_store: Some(Arc::new(BrowserStore { locks })),
|
recovery_store: Some(Arc::new(BrowserStore { locks })),
|
||||||
// File → Print: the browser's print dialog.
|
// File → Print: the browser's print dialog.
|
||||||
|
|
@ -517,7 +514,8 @@ fn download(path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||||
let revoke = wasm_bindgen::closure::Closure::once_into_js(move || {
|
let revoke = wasm_bindgen::closure::Closure::once_into_js(move || {
|
||||||
web_sys::Url::revoke_object_url(&url).ok();
|
web_sys::Url::revoke_object_url(&url).ok();
|
||||||
});
|
});
|
||||||
window.set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 10_000).map_err(js)?;
|
// The download has started: a timer that can't be set leaves the URL allocated, no more.
|
||||||
|
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 10_000);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,10 @@ mod tests {
|
||||||
let written = Written::default();
|
let written = Written::default();
|
||||||
let w = written.clone();
|
let w = written.clone();
|
||||||
let services = Services {
|
let services = Services {
|
||||||
download: Some(Box::new(move |name: &str, b: &[u8]| w.borrow_mut().push((name.to_string(), b.to_vec())))),
|
download: Some(Box::new(move |name: &str, b: &[u8]| {
|
||||||
|
w.borrow_mut().push((name.to_string(), b.to_vec()));
|
||||||
|
Ok(())
|
||||||
|
})),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
(with_art(services), written)
|
(with_art(services), written)
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,10 @@ fn the_web_downloads_a_zip() {
|
||||||
let mut app = saved(&dir);
|
let mut app = saved(&dir);
|
||||||
let got: Rc<RefCell<Vec<(String, usize)>>> = Rc::default();
|
let got: Rc<RefCell<Vec<(String, usize)>>> = Rc::default();
|
||||||
let g = got.clone();
|
let g = got.clone();
|
||||||
app.services.download = Some(Box::new(move |name: &str, b: &[u8]| g.borrow_mut().push((name.to_string(), b.len()))));
|
app.services.download = Some(Box::new(move |name: &str, b: &[u8]| {
|
||||||
|
g.borrow_mut().push((name.to_string(), b.len()));
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
app.run("ui.packageDialog", json!({})).unwrap();
|
app.run("ui.packageDialog", json!({})).unwrap();
|
||||||
assert!(!frame(&mut app).contains("Location:"), "no folder to choose");
|
assert!(!frame(&mut app).contains("Location:"), "no folder to choose");
|
||||||
confirm(&mut app).unwrap();
|
confirm(&mut app).unwrap();
|
||||||
|
|
@ -124,7 +127,7 @@ fn fonts_left_out_of_the_package_are_named() {
|
||||||
assert!(detail.contains(gone) && !detail.contains("Source Sans 3"), "{detail}");
|
assert!(detail.contains(gone) && !detail.contains("Source Sans 3"), "{detail}");
|
||||||
// The web's download says so too.
|
// The web's download says so too.
|
||||||
app.ui.dialog = None;
|
app.ui.dialog = None;
|
||||||
app.services.download = Some(Box::new(|_: &str, _: &[u8]| {}));
|
app.services.download = Some(Box::new(|_: &str, _: &[u8]| Ok(())));
|
||||||
app.run("ui.packageDialog", json!({})).unwrap();
|
app.run("ui.packageDialog", json!({})).unwrap();
|
||||||
confirm(&mut app).unwrap();
|
confirm(&mut app).unwrap();
|
||||||
assert!(app.ui.status.contains(gone), "{}", app.ui.status);
|
assert!(app.ui.status.contains(gone), "{}", app.ui.status);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ fn app(dir: &std::path::Path, web: bool) -> (VectorcraftApp, Log) {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
if web {
|
if web {
|
||||||
services.download = Some(Box::new(move |name: &str, b: &[u8]| l2.borrow_mut().push(format!("download {name} {}", b.len()))));
|
services.download = Some(Box::new(move |name: &str, b: &[u8]| {
|
||||||
|
l2.borrow_mut().push(format!("download {name} {}", b.len()));
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
let mut app = VectorcraftApp::new(Session::new(), services);
|
let mut app = VectorcraftApp::new(Session::new(), services);
|
||||||
app.run("file.new", json!({"width": 60, "height": 40})).unwrap();
|
app.run("file.new", json!({"width": 60, "height": 40})).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@ fn app(web: bool) -> (VectorcraftApp, Log, [u64; 2]) {
|
||||||
let l = log.clone();
|
let l = log.clone();
|
||||||
let mut services = Services::default();
|
let mut services = Services::default();
|
||||||
if web {
|
if web {
|
||||||
services.download = Some(Box::new(move |name: &str, bytes: &[u8]| l.borrow_mut().push((name.to_string(), bytes.to_vec()))));
|
services.download = Some(Box::new(move |name: &str, bytes: &[u8]| {
|
||||||
|
l.borrow_mut().push((name.to_string(), bytes.to_vec()));
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
services.reveal = Some(Box::new(move |path: &str| {
|
services.reveal = Some(Box::new(move |path: &str| {
|
||||||
l.borrow_mut().push((path.to_string(), vec![]));
|
l.borrow_mut().push((path.to_string(), vec![]));
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@ fn app(artboards: usize, web: bool) -> (VectorcraftApp, Log) {
|
||||||
let l = log.clone();
|
let l = log.clone();
|
||||||
let mut services = Services::default();
|
let mut services = Services::default();
|
||||||
if web {
|
if web {
|
||||||
services.download = Some(Box::new(move |name: &str, bytes: &[u8]| l.borrow_mut().push((name.to_string(), bytes.to_vec()))));
|
services.download = Some(Box::new(move |name: &str, bytes: &[u8]| {
|
||||||
|
l.borrow_mut().push((name.to_string(), bytes.to_vec()));
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
services.reveal = Some(Box::new(move |path: &str| {
|
services.reveal = Some(Box::new(move |path: &str| {
|
||||||
l.borrow_mut().push((path.to_string(), vec![]));
|
l.borrow_mut().push((path.to_string(), vec![]));
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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)
|
||||||
|
|
@ -239,8 +262,7 @@ pub fn new_from_template(app: &mut VectorcraftApp, path: Option<String>) -> Resu
|
||||||
/// Web: download `bytes` under `path`'s file name; desktop: write them to `path`.
|
/// Web: download `bytes` under `path`'s file name; desktop: write them to `path`.
|
||||||
pub(crate) fn write_to(services: &mut Services, path: &str, bytes: &[u8]) -> Result<(), String> {
|
pub(crate) fn write_to(services: &mut Services, path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||||
if let Some(dl) = services.download.as_mut() {
|
if let Some(dl) = services.download.as_mut() {
|
||||||
dl(&fileio::file_name(path), bytes);
|
return dl(&fileio::file_name(path), bytes);
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
services.write.as_mut().ok_or("no file writer")?(path, bytes)
|
services.write.as_mut().ok_or("no file writer")?(path, bytes)
|
||||||
}
|
}
|
||||||
|
|
@ -260,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -516,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();
|
||||||
|
|
@ -602,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(),
|
||||||
|
|
@ -673,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -768,7 +793,7 @@ pub(crate) fn export_files(app: &mut VectorcraftApp, id: &str, params: Value) ->
|
||||||
} else if let Some(download) = app.services.download.as_mut() {
|
} else if let Some(download) = app.services.download.as_mut() {
|
||||||
for (name, data) in &returned {
|
for (name, data) in &returned {
|
||||||
let bytes = vectorcraft_format::base64_decode(data).ok_or("the export returned unreadable data")?;
|
let bytes = vectorcraft_format::base64_decode(data).ok_or("the export returned unreadable data")?;
|
||||||
download(name, &bytes);
|
download(name, &bytes)?;
|
||||||
}
|
}
|
||||||
let what = match returned.as_slice() {
|
let what = match returned.as_slice() {
|
||||||
[(name, _)] => (*name).to_string(),
|
[(name, _)] => (*name).to_string(),
|
||||||
|
|
@ -830,6 +855,91 @@ 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
|
||||||
|
/// 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 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();
|
||||||
|
let unsaved = |app: &VectorcraftApp| {
|
||||||
|
let st = app.session.doc().unwrap();
|
||||||
|
(st.is_dirty(), st.path.clone())
|
||||||
|
};
|
||||||
|
let e = save(&mut app, SaveMode::Save, &json!({"path": "Poster.vectorcraft"}), false).unwrap_err();
|
||||||
|
assert!(e.contains("the browser refused the download"), "{e}");
|
||||||
|
assert_eq!(unsaved(&app), (true, None));
|
||||||
|
// Save Changes › Save: not saved, so not closed.
|
||||||
|
let (i, uid) = (app.session.active_index().unwrap(), app.session.doc().unwrap().uid);
|
||||||
|
app.ui.dialog = Some(crate::state::Dialog::new(crate::unsaved::KIND, json!({"index": i, "uid": uid, "name": "Poster", "then": "close"})));
|
||||||
|
app.session.doc_mut().unwrap().path = Some("Poster.vectorcraft".into());
|
||||||
|
assert!(crate::unsaved::confirm(&mut app).is_err());
|
||||||
|
assert_eq!((app.session.documents().len(), app.session.doc().unwrap().is_dirty()), (1, true));
|
||||||
|
// Once the download starts, the save counts.
|
||||||
|
*started.borrow_mut() = true;
|
||||||
|
save(&mut app, SaveMode::Save, &json!({}), false).unwrap();
|
||||||
|
assert!(!app.session.doc().unwrap().is_dirty());
|
||||||
|
}
|
||||||
|
|
||||||
fn bytes_of(app: &mut VectorcraftApp, cmd: &str, p: Value) -> Vec<u8> {
|
fn bytes_of(app: &mut VectorcraftApp, cmd: &str, p: Value) -> Vec<u8> {
|
||||||
let v = app.session.execute(cmd, &p).unwrap();
|
let v = app.session.execute(cmd, &p).unwrap();
|
||||||
vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap()
|
vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap()
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,9 @@ pub type PickSave = Box<dyn FnMut(&FilePick) -> Option<String>>;
|
||||||
pub type ReadFn = Box<dyn Fn(&str) -> Result<Vec<u8>, String>>;
|
pub type ReadFn = Box<dyn Fn(&str) -> Result<Vec<u8>, String>>;
|
||||||
pub type WriteFn = Box<dyn FnMut(&str, &[u8]) -> Result<(), String>>;
|
pub type WriteFn = Box<dyn FnMut(&str, &[u8]) -> Result<(), String>>;
|
||||||
pub type Inbox = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
|
pub type Inbox = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
|
||||||
pub type DownloadFn = Box<dyn FnMut(&str, &[u8])>;
|
/// Starts a browser download of the bytes under a file name; an error when it couldn't be started
|
||||||
|
/// (the save or export then fails as a refused write does: #1693).
|
||||||
|
pub type DownloadFn = Box<dyn FnMut(&str, &[u8]) -> Result<(), String>>;
|
||||||
|
|
||||||
/// Opens a URL in the system browser.
|
/// Opens a URL in the system browser.
|
||||||
pub type OpenUrlFn = Box<dyn FnMut(&str)>;
|
pub type OpenUrlFn = Box<dyn FnMut(&str)>;
|
||||||
|
|
@ -778,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();
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,14 @@ pub fn start(app: &mut VectorcraftApp, path: &str, bytes: Vec<u8>) -> Result<Val
|
||||||
Ok(json!({ "background": true, "status": status }))
|
Ok(json!({ "background": true, "status": status }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open the files that are ready (each frame).
|
/// Open the files that are ready (each frame). Not while a dialog is open: a document that opens
|
||||||
|
/// becomes the active one, and the dialog's OK acts on the active document (#1694: Save As
|
||||||
|
/// options confirmed after another file had finished opening saved that file's art under the
|
||||||
|
/// first one's name). The files wait, their status line too, until the dialog is closed.
|
||||||
pub fn poll(app: &mut VectorcraftApp) {
|
pub fn poll(app: &mut VectorcraftApp) {
|
||||||
|
if app.ui.dialog.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while let Some(p) = app.opening.pending.get(i) {
|
while let Some(p) = app.opening.pending.get(i) {
|
||||||
let r = match p.done.try_recv() {
|
let r = match p.done.try_recv() {
|
||||||
|
|
@ -108,6 +114,32 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::Services;
|
use crate::Services;
|
||||||
|
|
||||||
|
/// #1694: a file that finishes opening while a dialog is up waits for it to close, so the
|
||||||
|
/// dialog's document stays the active one.
|
||||||
|
#[test]
|
||||||
|
fn a_file_ready_while_a_dialog_is_open_waits_for_it_to_close() {
|
||||||
|
let mut app = VectorcraftApp::new(Session::new(), Services::default());
|
||||||
|
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
|
||||||
|
let (first, before) = (app.session.doc().unwrap().uid, app.session.documents().len());
|
||||||
|
app.ui.dialog = Some(crate::state::Dialog::new(crate::dialogs::save_options::KIND, json!({"path": "/art/First.vectorcraft"})));
|
||||||
|
start(&mut app, "/art/Second.vectorcraft", vectorcraft_format::save(&vectorcraft_doc::Document::new(120.0, 80.0), false)).unwrap();
|
||||||
|
let t0 = std::time::Instant::now();
|
||||||
|
while t0.elapsed().as_millis() < 400 {
|
||||||
|
poll(&mut app);
|
||||||
|
std::thread::yield_now();
|
||||||
|
}
|
||||||
|
assert_eq!((app.session.documents().len(), app.session.doc().unwrap().uid), (before, first), "still the dialog's document");
|
||||||
|
assert_eq!(app.opening.pending.len(), 1);
|
||||||
|
app.ui.dialog = None;
|
||||||
|
let t0 = std::time::Instant::now();
|
||||||
|
while !app.opening.pending.is_empty() && t0.elapsed().as_secs() < 30 {
|
||||||
|
poll(&mut app);
|
||||||
|
std::thread::yield_now();
|
||||||
|
}
|
||||||
|
assert_eq!(app.session.documents().len(), before + 1);
|
||||||
|
assert_eq!(app.session.doc().unwrap().path.as_deref(), Some("/art/Second.vectorcraft"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_file_read_off_the_ui_thread_opens_as_one_read_at_once() {
|
fn a_file_read_off_the_ui_thread_opens_as_one_read_at_once() {
|
||||||
let mut app = VectorcraftApp::new(Session::new(), Services::default());
|
let mut app = VectorcraftApp::new(Session::new(), Services::default());
|
||||||
|
|
|
||||||
|
|
@ -646,8 +646,13 @@ mod tests {
|
||||||
// The web downloads the file under that name.
|
// The web downloads the file under that name.
|
||||||
let downloads = std::rc::Rc::new(std::cell::RefCell::new(vec![]));
|
let downloads = std::rc::Rc::new(std::cell::RefCell::new(vec![]));
|
||||||
let dl = downloads.clone();
|
let dl = downloads.clone();
|
||||||
let services =
|
let services = crate::Services {
|
||||||
crate::Services { download: Some(Box::new(move |name: &str, _: &[u8]| dl.borrow_mut().push(name.to_string()))), ..Default::default() };
|
download: Some(Box::new(move |name: &str, _: &[u8]| {
|
||||||
|
dl.borrow_mut().push(name.to_string());
|
||||||
|
Ok(())
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
let mut web = VectorcraftApp::new(Session::new(), services);
|
let mut web = VectorcraftApp::new(Session::new(), services);
|
||||||
web.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
web.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||||
web.run("ui.saveSwatchLibrary", json!({})).unwrap();
|
web.run("ui.saveSwatchLibrary", json!({})).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -155,11 +155,12 @@ pub(crate) fn go_to(app: &mut VectorcraftApp, id: u64) {
|
||||||
|
|
||||||
/// Write embedded image `id` to a picked file (the web downloads it) and link it there.
|
/// Write embedded image `id` to a picked file (the web downloads it) and link it there.
|
||||||
pub(crate) fn unembed(app: &mut VectorcraftApp, id: u64, name: &str) {
|
pub(crate) fn unembed(app: &mut VectorcraftApp, id: u64, name: &str) {
|
||||||
if let Some(dl) = app.services.download.as_mut() {
|
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))
|
||||||
{
|
{
|
||||||
dl(name, &bytes);
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@ fn app(web: bool) -> (VectorcraftApp, Log, [u64; 2]) {
|
||||||
let l = log.clone();
|
let l = log.clone();
|
||||||
let mut services = Services::default();
|
let mut services = Services::default();
|
||||||
if web {
|
if web {
|
||||||
services.download = Some(Box::new(move |name: &str, bytes: &[u8]| l.borrow_mut().push((name.to_string(), bytes.to_vec()))));
|
services.download = Some(Box::new(move |name: &str, bytes: &[u8]| {
|
||||||
|
l.borrow_mut().push((name.to_string(), bytes.to_vec()));
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
let mut app = VectorcraftApp::new(Session::new(), services);
|
let mut app = VectorcraftApp::new(Session::new(), services);
|
||||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -485,10 +485,7 @@ pub fn run_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Resu
|
||||||
// Its save dialog is open: the file is written when it answers.
|
// Its save dialog is open: the file is written when it answers.
|
||||||
None if app.file_dialog_open() => Err("cancelled".into()),
|
None if app.file_dialog_open() => Err("cancelled".into()),
|
||||||
None => match app.services.download.as_mut() {
|
None => match app.services.download.as_mut() {
|
||||||
Some(dl) => {
|
Some(dl) => dl("VectorCraft Shortcuts.json", &bytes).map(|()| Value::Null),
|
||||||
dl("VectorCraft Shortcuts.json", &bytes);
|
|
||||||
Ok(Value::Null)
|
|
||||||
}
|
|
||||||
None => Ok(v),
|
None => Ok(v),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,10 @@ fn the_web_save_as_dialog_shows_the_native_options() {
|
||||||
let written = crate::tests_svg::Written::default();
|
let written = crate::tests_svg::Written::default();
|
||||||
let w = written.clone();
|
let w = written.clone();
|
||||||
let services = crate::Services {
|
let services = crate::Services {
|
||||||
download: Some(Box::new(move |name: &str, b: &[u8]| w.borrow_mut().push((name.to_string(), b.to_vec())))),
|
download: Some(Box::new(move |name: &str, b: &[u8]| {
|
||||||
|
w.borrow_mut().push((name.to_string(), b.to_vec()));
|
||||||
|
Ok(())
|
||||||
|
})),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), services);
|
let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), services);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue