diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 251ce9005..8be3aafb5 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3576,7 +3576,16 @@ pub fn confirm(app: &mut DesignApp) -> Result { } "alert" => Ok(Value::Null), crate::effects_dialog::ID => crate::effects_dialog::confirm(app, &d), - crate::object_style_dialog::ID => crate::object_style_dialog::confirm(app, &d), + crate::object_style_dialog::ID => { + let r = crate::object_style_dialog::confirm(app, &d); + if let Err(e) = &r { + // Keep the dialog and its edits open with the reason (an empty or taken name). + let mut d = d.clone(); + d.fields.insert("status".into(), json!(e)); + app.ui.dialog = Some(d); + } + r + } "findChange" => { let result = if d.b("objectMode") { Ok(Value::Null) } else { app.run("find.change", find_change_params(&d)) }; // Confirmation consumes retained bounds before closing, even on an error. diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 64261a4de..b46318852 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -32,14 +32,14 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Copy", "Kopírovat"), ("Paste", "Vložit"), ("New Object Style", "Nový styl objektu"), - ("Object Style Options", "Volby stylu objektu"), - ("Object Style Options…", "Volby stylu objektu…"), + ("Object Style Options", "Možnosti stylu objektu"), + ("Object Style Options…", "Možnosti stylu objektu…"), ("Basic Attributes", "Základní atributy"), - ("Stroke & Corner Options", "Volby tahu a rohů"), + ("Stroke & Corner Options", "Možnosti tahu a rohů"), ("Effects for:", "Efekty pro:"), ("Apply Style to Selection", "Použít styl na výběr"), ("Create New Style", "Vytvořit nový styl"), - ("Clear Overrides", "Vymazat změny"), + ("Clear Overrides", "Zrušit přepsání"), ("Shape:", "Tvar:"), ("(no attributes)", "(žádné atributy)"), ("Enter a style name.", "Zadejte název stylu."), diff --git a/crates/ui-egui/src/object_style_dialog.rs b/crates/ui-egui/src/object_style_dialog.rs index b9e1b3ce5..038e9b7b4 100644 --- a/crates/ui-egui/src/object_style_dialog.rs +++ b/crates/ui-egui/src/object_style_dialog.rs @@ -99,8 +99,13 @@ pub fn confirm(app: &mut DesignApp, d: &Dialog) -> Result { p.insert("fromSelection".into(), json!(false)); let r = app.run("style.object.create", Value::Object(p)).map_err(|e| e.to_string())?; let made = r["name"].as_str().unwrap_or(&name).to_string(); - if d.b("apply") && app.session.active().is_some_and(|st| !st.selection.items.is_empty()) { - app.run("style.object.apply", json!({"name": made})).map_err(|e| e.to_string())?; + // The style exists now, so OK again mustn't make a second one: a refused apply (locked + // objects) goes to the status line and the dialog closes. + if d.b("apply") + && app.session.active().is_some_and(|st| !st.selection.items.is_empty()) + && let Err(e) = app.run("style.object.apply", json!({"name": made})) + { + app.status(e.to_string()); } Ok(r) } else { @@ -324,6 +329,11 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { d.fields.insert("name".into(), json!(name)); } }); + // Why OK was refused (an empty or taken name), until the next try. + let status = d.s("status"); + if !status.is_empty() { + crate::rtl::label(ui, egui::RichText::new(status).color(crate::theme::Tokens::get(ui.ctx()).text_strong)); + } ui.add_space(8.0); let page = d.fields.get("page").and_then(Value::as_str).unwrap_or("general").to_string(); ui.horizontal_top(|ui| { @@ -464,4 +474,32 @@ mod tests { assert!(doc(&app).styles.object_style(name).unwrap().fill.is_none(), "Fill unchecked: no longer in the style"); assert!(doc(&app).styles.object_style(name).unwrap().effects.as_ref().unwrap().drop_shadow.on, "effects kept"); } + + /// A refused OK (an empty name, a name another style has) keeps the dialog and its edits, + /// with the reason shown. + #[test] + fn a_refused_ok_keeps_the_dialog_open_with_the_reason() { + let (mut app, _) = app_with_frame(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx, "en"); + app.run("style.object.create", json!({"name": "Taken", "fromSelection": false})).unwrap(); + app.run("style.object.create", json!({"name": "Mine", "fromSelection": false})).unwrap(); + open_new(&mut app); + if let Some(d) = app.ui.dialog.as_mut() { + d.fields.insert("name".into(), json!(" ")); + set(d, &["stroke", "weight"], json!(7.0)); + } + assert!(crate::dialogs::confirm(&mut app).is_err()); + let d = app.ui.dialog.as_ref().expect("still open"); + assert_eq!(d.s("status"), "Enter a style name."); + assert_eq!(get(d, &["stroke", "weight"]), &json!(7.0), "the edits are kept"); + assert!(frame(&mut app, &ctx).iter().any(|t| t.trim() == "Enter a style name.")); + open_edit(&mut app, "Mine"); + if let Some(d) = app.ui.dialog.as_mut() { + d.fields.insert("name".into(), json!("Taken")); + } + let e = crate::dialogs::confirm(&mut app).unwrap_err(); + assert!(e.contains("already named"), "{e}"); + assert!(app.ui.dialog.as_ref().is_some_and(|d| d.s("status") == e)); + } }