mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Save and Export ask before replacing a file whose import left things out
The engine refuses to write over the file a document was read from when
reading it left something out, unless acknowledgeLoss is passed. In the
app that surfaced as an error naming a parameter. File > Save, Save As,
Save a Copy, Save as Template and Export As now ask instead ("Replace
'a.eps', the file this document was opened from?", with what would be
lost), and OK repeats the command with acknowledgeLoss. Closing a
document whose save asks keeps it open, and so does Package.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5f4f82d885
commit
0559aac53e
@@ -395,14 +395,8 @@ fn is_loss(note: &str) -> bool {
|
||||
/// it left something out (hidden text, art or layers), unless `p` has `acknowledgeLoss: true`.
|
||||
/// Another name keeps the original file, with all it has.
|
||||
pub(crate) fn check_not_lossy_overwrite(st: &crate::DocState, target: &str, p: &Value, cmd: &str) -> Result<()> {
|
||||
let Some(source) = st.imported_from.as_deref().filter(|_| !st.import_losses.is_empty()) else { return Ok(()) };
|
||||
if bool_or(p, "acknowledgeLoss", false) || !same_file(source, target) {
|
||||
return Ok(());
|
||||
}
|
||||
let mut what = st.import_losses.iter().take(3).map(|n| format!("“{n}”")).collect::<Vec<_>>().join("; ");
|
||||
if st.import_losses.len() > 3 {
|
||||
what.push_str(&format!("; {} more", st.import_losses.len() - 3));
|
||||
}
|
||||
let Some(losses) = overwrite_losses(st, target, p) else { return Ok(()) };
|
||||
let what = losses_summary(losses);
|
||||
Err(bad(
|
||||
cmd,
|
||||
format!(
|
||||
@@ -411,6 +405,23 @@ pub(crate) fn check_not_lossy_overwrite(st: &crate::DocState, target: &str, p: &
|
||||
))
|
||||
}
|
||||
|
||||
/// What writing the file `target` for document `st` would lose for good: the notes of what reading
|
||||
/// it left out when `target` is the file `st` was read from, unless `p` has `acknowledgeLoss: true`.
|
||||
/// The UI asks before such a write and repeats it with `acknowledgeLoss` ([`check_not_lossy_overwrite`]).
|
||||
pub fn overwrite_losses<'a>(st: &'a crate::DocState, target: &str, p: &Value) -> Option<&'a [String]> {
|
||||
let source = st.imported_from.as_deref().filter(|_| !st.import_losses.is_empty())?;
|
||||
(!bool_or(p, "acknowledgeLoss", false) && same_file(source, target)).then_some(st.import_losses.as_slice())
|
||||
}
|
||||
|
||||
/// Up to three of `losses`, quoted, and how many more.
|
||||
pub fn losses_summary(losses: &[String]) -> String {
|
||||
let mut what = losses.iter().take(3).map(|n| format!("“{n}”")).collect::<Vec<_>>().join("; ");
|
||||
if losses.len() > 3 {
|
||||
what.push_str(&format!("; {} more", losses.len() - 3));
|
||||
}
|
||||
what
|
||||
}
|
||||
|
||||
/// Do `a` and `b` name one file (spelled differently, or through a link)?
|
||||
fn same_file(a: &str, b: &str) -> bool {
|
||||
match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
|
||||
|
||||
@@ -53,6 +53,7 @@ pub(crate) use load::import_svg;
|
||||
pub(crate) use load::native_file;
|
||||
pub(crate) use load::source;
|
||||
pub use load::{Loaded, RasterImage, detect, file_name, load, load_with, open_bytes, open_bytes_with, open_template, raster_image};
|
||||
pub use load::{losses_summary, overwrite_losses};
|
||||
pub use native::with_compression_pref;
|
||||
pub use pdfimport::{LoadOptions, page_document};
|
||||
pub(crate) use save::job_for;
|
||||
|
||||
@@ -104,7 +104,11 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
}
|
||||
// The package holds the document as saved.
|
||||
if app.session.active().is_some_and(|st| st.is_dirty()) {
|
||||
io::save(app, vectorcraft_engine::cmd::fileio::SaveMode::Save, &json!({}), false)?;
|
||||
let r = io::save(app, vectorcraft_engine::cmd::fileio::SaveMode::Save, &json!({}), false)?;
|
||||
// The save asks first (it would replace a file that loses what opening it left out).
|
||||
if r.get("pending").is_some() {
|
||||
return Ok(r);
|
||||
}
|
||||
}
|
||||
let r = app.run("file.package", params)?;
|
||||
app.ui.dialog = None;
|
||||
|
||||
@@ -2147,6 +2147,8 @@
|
||||
Reverse Spine Invertir eje
|
||||
Revert Revertir
|
||||
Revert to the saved version of “{name}”? ¿Revertir a la versión guardada de “{name}”?
|
||||
Replace “{name}”, the file this document was opened from? ¿Reemplazar “{name}”, el archivo desde el que se abrió este documento?
|
||||
Opening it left out what VectorCraft can't read yet ({what}), so replacing it loses that for good. Save under another name to keep it. Al abrirlo quedó fuera lo que VectorCraft aún no puede leer ({what}), así que reemplazarlo lo pierde para siempre. Guárdelo con otro nombre para conservarlo.
|
||||
Revolve… Revolución…
|
||||
RGB Color Color RGB
|
||||
Right Derecha
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2147,6 +2147,8 @@
|
||||
Reverse Spine Inverti dorsale
|
||||
Revert Ripristina
|
||||
Revert to the saved version of “{name}”? Ripristinare la versione salvata di “{name}”?
|
||||
Replace “{name}”, the file this document was opened from? Sostituire “{name}”, il file da cui è stato aperto questo documento?
|
||||
Opening it left out what VectorCraft can't read yet ({what}), so replacing it loses that for good. Save under another name to keep it. Aprendolo è rimasto fuori ciò che VectorCraft non sa ancora leggere ({what}), quindi sostituirlo lo perde per sempre. Salva con un altro nome per conservarlo.
|
||||
Revolve… Rivoluzione…
|
||||
RGB Color Colore RGB
|
||||
Right Destra
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2115,6 +2115,8 @@
|
||||
Reverse Path Direction Off パスの方向を反転: オフ
|
||||
Reverse Path Direction On パスの方向を反転: オン
|
||||
Revert to the saved version of “{name}”? 「{name}」を保存済みの状態に戻しますか?
|
||||
Replace “{name}”, the file this document was opened from? このドキュメントの開き元のファイル「{name}」を置き換えますか?
|
||||
Opening it left out what VectorCraft can't read yet ({what}), so replacing it loses that for good. Save under another name to keep it. 開いたときに VectorCraft がまだ読み込めない内容({what})が除外されたため、置き換えるとそれは完全に失われます。保持するには別の名前で保存してください。
|
||||
Right Grid: 右グリッド:
|
||||
Right Indent 右インデント
|
||||
Right Plane 右面
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1904,6 +1904,8 @@
|
||||
Reverse Spine Inverter Coluna
|
||||
Revert Reverter
|
||||
Revert to the saved version of “{name}”? Reverter para a versão salva de “{name}”?
|
||||
Replace “{name}”, the file this document was opened from? Substituir “{name}”, o arquivo do qual este documento foi aberto?
|
||||
Opening it left out what VectorCraft can't read yet ({what}), so replacing it loses that for good. Save under another name to keep it. Ao abri-lo, ficou de fora o que o VectorCraft ainda não consegue ler ({what}), então substituí-lo perde isso para sempre. Salve com outro nome para mantê-lo.
|
||||
Revolve… Revolver…
|
||||
Right Grid: Grade Direita:
|
||||
Right Indent Recuo Direito
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2143,6 +2143,8 @@
|
||||
Reverse Spine Обратить остов
|
||||
Revert Вернуть
|
||||
Revert to the saved version of “{name}”? Вернуться к сохранённой версии «{name}»?
|
||||
Replace “{name}”, the file this document was opened from? Заменить «{name}» — файл, из которого открыт этот документ?
|
||||
Opening it left out what VectorCraft can't read yet ({what}), so replacing it loses that for good. Save under another name to keep it. При открытии было пропущено то, что VectorCraft пока не умеет читать ({what}), поэтому замена файла потеряет это навсегда. Чтобы сохранить это, сохраните документ под другим именем.
|
||||
Revolve… Вращение…
|
||||
RGB Color Цвет RGB
|
||||
Right Справа
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2102,6 +2102,8 @@
|
||||
Reverse Spine 反转路径
|
||||
Revert 恢复
|
||||
Revert to the saved version of “{name}”? 要恢复为“{name}”已存储的版本吗?
|
||||
Replace “{name}”, the file this document was opened from? 要替换“{name}”(打开此文档的源文件)吗?
|
||||
Opening it left out what VectorCraft can't read yet ({what}), so replacing it loses that for good. Save under another name to keep it. 打开时略去了 VectorCraft 尚无法读取的内容({what}),替换该文件会永久丢失这些内容。要保留它们,请以其他名称存储。
|
||||
Revolve… 回转…
|
||||
RGB Color RGB 颜色
|
||||
Right 右
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2150,6 +2150,8 @@
|
||||
Reverse Spine 反轉軸線
|
||||
Revert 回復
|
||||
Revert to the saved version of “{name}”? 要回復為「{name}」已儲存的版本嗎?
|
||||
Replace “{name}”, the file this document was opened from? 要取代「{name}」(開啟此文件的來源檔案)嗎?
|
||||
Opening it left out what VectorCraft can't read yet ({what}), so replacing it loses that for good. Save under another name to keep it. 開啟時略過了 VectorCraft 尚無法讀取的內容({what}),取代該檔案會永久遺失這些內容。若要保留,請以其他名稱儲存。
|
||||
Revolve… 迴轉…
|
||||
RGB Color RGB 色彩
|
||||
Right 右
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -239,9 +239,13 @@ fn plan(app: &VectorcraftApp, mode: SaveMode, p: &Value) -> Result<SavePlan, Str
|
||||
/// path?}` while a dialog is open.
|
||||
pub fn save(app: &mut VectorcraftApp, mode: SaveMode, p: &Value, ask_options: bool) -> Result<Value, String> {
|
||||
remember_view(app);
|
||||
let first = plan(app, mode, p)?;
|
||||
// Writing over the file the document was read from is asked about here, not refused.
|
||||
let first = plan(app, mode, &acknowledged(p))?;
|
||||
let ask = ask_options && !has_options(p);
|
||||
if let Some(path) = first.path.clone() {
|
||||
if let Some(r) = ask_before_losing(app, &path, save_command(mode), p) {
|
||||
return Ok(r);
|
||||
}
|
||||
if ask && matches!(mode, SaveMode::SaveAs | SaveMode::Copy) && matches!(first.format.id, "svg" | "svgz") {
|
||||
return ask_format_options(app, mode, first.format, &path);
|
||||
}
|
||||
@@ -261,7 +265,10 @@ pub fn save(app: &mut VectorcraftApp, mode: SaveMode, p: &Value, ask_options: bo
|
||||
o.remove("format");
|
||||
o.insert("path".into(), json!(with_save_extension(&picked, first.format)));
|
||||
}
|
||||
let chosen = plan(app, mode, &q)?;
|
||||
let chosen = plan(app, mode, &acknowledged(&q))?;
|
||||
if let Some(r) = chosen.path.as_deref().and_then(|path| ask_before_losing(app, path, save_command(mode), &q)) {
|
||||
return Ok(r);
|
||||
}
|
||||
if ask && asks_options(mode, chosen.format) {
|
||||
let path = chosen.path.clone().unwrap_or_default();
|
||||
return ask_format_options(app, mode, chosen.format, &path);
|
||||
@@ -269,6 +276,42 @@ pub fn save(app: &mut VectorcraftApp, mode: SaveMode, p: &Value, ask_options: bo
|
||||
write_plan(app, chosen)
|
||||
}
|
||||
|
||||
/// The UI command that runs a save in `mode`.
|
||||
fn save_command(mode: SaveMode) -> &'static str {
|
||||
match mode {
|
||||
SaveMode::Save => "file.save",
|
||||
SaveMode::SaveAs => "file.saveAs",
|
||||
SaveMode::Copy => "file.saveCopy",
|
||||
SaveMode::Template => "file.saveAsTemplate",
|
||||
}
|
||||
}
|
||||
|
||||
/// `p` with `acknowledgeLoss: true`.
|
||||
fn acknowledged(p: &Value) -> Value {
|
||||
let mut p = if p.is_object() { p.clone() } else { json!({}) };
|
||||
p["acknowledgeLoss"] = json!(true);
|
||||
p
|
||||
}
|
||||
|
||||
/// Writing `path` over the file the active document was read from, when reading it left things
|
||||
/// out (hidden text, art or layers VectorCraft can't read yet): asks first, and OK runs `command`
|
||||
/// with `params` and `acknowledgeLoss` → `{pending}` while it asks, `None` when nothing is lost.
|
||||
fn ask_before_losing(app: &mut VectorcraftApp, path: &str, command: &str, params: &Value) -> Option<Value> {
|
||||
let what = fileio::losses_summary(fileio::overwrite_losses(app.session.active()?, path, params)?);
|
||||
let name = fileio::file_name(path);
|
||||
let message = crate::i18n::fmt(tl!("Replace “{name}”, the file this document was opened from?"), &[("name", &name)]);
|
||||
let detail = crate::i18n::fmt(
|
||||
tl!("Opening it left out what VectorCraft can't read yet ({what}), so replacing it loses that for good. Save under another name to keep it."),
|
||||
&[("what", &what)],
|
||||
);
|
||||
let mut params = acknowledged(params);
|
||||
if command != "file.save" {
|
||||
params["path"] = json!(path);
|
||||
}
|
||||
dialogs::confirm::ask(app, &message, &detail, command, params);
|
||||
Some(json!({ "pending": dialogs::confirm::KIND }))
|
||||
}
|
||||
|
||||
/// Does a save (`mode`) to a picked file of format `f` ask for its options first? Native and `.ai`
|
||||
/// files only on Save As (the options dialog after the save panel); Save, a Copy and templates
|
||||
/// reuse the remembered options or Use Compression.
|
||||
@@ -421,6 +464,13 @@ pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option<Strin
|
||||
std::borrow::Cow::Owned(d) => std::sync::Arc::new(d),
|
||||
};
|
||||
let path = target_path(app, path, f.extensions[0])?;
|
||||
let mut again = params.clone();
|
||||
if let Some(o) = again.as_object_mut() {
|
||||
o.insert("format".into(), json!(f.id));
|
||||
}
|
||||
if let Some(r) = ask_before_losing(app, &path, "file.exportAs", &again) {
|
||||
return Ok(r);
|
||||
}
|
||||
// An SVG given a .svgz name is written compressed.
|
||||
let f = match fileio::format_for_name(&path) {
|
||||
Some(z) if f.id == "svg" && z.id == "svgz" => z,
|
||||
|
||||
@@ -166,3 +166,42 @@ fn a_document_reopens_at_its_saved_view() {
|
||||
assert!(!app.view().unwrap().fitted);
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacing_a_file_that_opening_left_things_out_of_asks_first() {
|
||||
let d = dir("lossy");
|
||||
let path = d.join("a.vectorcraft").to_string_lossy().to_string();
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
rect(&mut app);
|
||||
app.run("file.saveAs", json!({"path": path})).unwrap();
|
||||
// As if reading the file had left out text it can't read yet.
|
||||
let st = app.session.doc_mut().unwrap();
|
||||
st.imported_from = Some(path.clone());
|
||||
st.import_losses = vec!["hidden type".into()];
|
||||
let saved = std::fs::read(&path).unwrap();
|
||||
rect(&mut app);
|
||||
let r = app.run("file.save", json!({})).unwrap();
|
||||
assert_eq!(r["pending"], dialogs::confirm::KIND);
|
||||
let dialog = app.ui.dialog.as_ref().unwrap();
|
||||
assert!(dialog.str("detail").contains("“hidden type”"), "{:?}", dialog.fields);
|
||||
assert_eq!(std::fs::read(&path).unwrap(), saved, "nothing is written before OK");
|
||||
// Export As over it asks too; Cancel leaves the file.
|
||||
app.ui.dialog = None;
|
||||
assert_eq!(app.run("file.exportAs", json!({"path": path, "format": "svg"})).unwrap()["pending"], dialogs::confirm::KIND);
|
||||
app.ui.dialog = None;
|
||||
assert_eq!(std::fs::read(&path).unwrap(), saved);
|
||||
// Another name doesn't ask.
|
||||
let copy = d.join("b.svg").to_string_lossy().to_string();
|
||||
assert!(app.run("file.exportAs", json!({"path": copy, "format": "svg"})).unwrap().get("pending").is_none());
|
||||
crate::background::wait_all(&mut app);
|
||||
assert!(app.ui.dialog.is_none());
|
||||
// OK replaces it.
|
||||
app.run("file.save", json!({})).unwrap();
|
||||
press_ok(&mut app);
|
||||
crate::background::wait_all(&mut app);
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_ne!(std::fs::read(&path).unwrap(), saved);
|
||||
assert!(!app.session.active().unwrap().is_dirty());
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,11 @@ pub fn confirm(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
if !d.bool("discard") {
|
||||
app.session.set_active(i);
|
||||
let r = io::save(app, vectorcraft_engine::cmd::fileio::SaveMode::Save, &json!({}), false)?;
|
||||
// A save that asks first (replacing a file that would lose what opening it left out) keeps
|
||||
// the document open: it closes once saved.
|
||||
if r.get("pending").is_some() {
|
||||
return Ok(r);
|
||||
}
|
||||
// Closing needs the file written: wait for a background save, and stop if it failed.
|
||||
if r["background"] == true {
|
||||
crate::background::wait_all(app);
|
||||
|
||||
Reference in New Issue
Block a user