diff --git a/crates/ui-egui/locales/de-formats.json b/crates/ui-egui/locales/de-formats.json index fd5e19861..a259b6936 100644 --- a/crates/ui-egui/locales/de-formats.json +++ b/crates/ui-egui/locales/de-formats.json @@ -215,5 +215,6 @@ "There is a “{name}” at the top level already. Merge the two?": "Auf der obersten Ebene gibt es bereits „{name}“. Beide zusammenführen?", "Added “{keyword}” to {n} photo{}": "„{keyword}“ zu {n} Foto{} hinzugefügt", "On {have} of {of} selected photos": "Auf {have} von {of} ausgewählten Fotos", - "Remove “{name}”": "„{name}“ entfernen" + "Remove “{name}”": "„{name}“ entfernen", + "There is a keyword set “{name}” already": "Es gibt bereits einen Stichwortsatz „{name}“" } diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index 3d153d3ce..a82080c9e 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1919,5 +1919,7 @@ "Edit Set…": "Satz bearbeiten…", "Edit Keyword Set": "Stichwortsatz bearbeiten", "Keywords (⌥1–⌥9)": "Stichwörter (⌥1–⌥9)", - "A keyword set needs a name": "Ein Stichwortsatz braucht einen Namen" + "A keyword set needs a name": "Ein Stichwortsatz braucht einen Namen", + "Save as a new set": "Als neuen Satz speichern", + "Recent Keywords is built in: choose another name": "„Letzte Stichwörter“ ist vorgegeben: Wählen Sie einen anderen Namen" } diff --git a/crates/ui-egui/locales/es-formats.json b/crates/ui-egui/locales/es-formats.json index dfd320993..04f321003 100644 --- a/crates/ui-egui/locales/es-formats.json +++ b/crates/ui-egui/locales/es-formats.json @@ -215,5 +215,6 @@ "There is a “{name}” at the top level already. Merge the two?": "Ya hay “{name}” en el nivel superior. ¿Combinar las dos?", "Added “{keyword}” to {n} photo{}": "Se añadió “{keyword}” a {n} foto{}", "On {have} of {of} selected photos": "En {have} de {of} fotos seleccionadas", - "Remove “{name}”": "Quitar “{name}”" + "Remove “{name}”": "Quitar “{name}”", + "There is a keyword set “{name}” already": "Ya hay un conjunto de palabras clave “{name}”" } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index e1781e252..7be7d02c2 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1764,5 +1764,7 @@ "Edit Set…": "Editar conjunto…", "Edit Keyword Set": "Editar conjunto de palabras clave", "Keywords (⌥1–⌥9)": "Palabras clave (⌥1–⌥9)", - "A keyword set needs a name": "Un conjunto de palabras clave necesita un nombre" + "A keyword set needs a name": "Un conjunto de palabras clave necesita un nombre", + "Save as a new set": "Guardar como conjunto nuevo", + "Recent Keywords is built in: choose another name": "“Palabras clave recientes” es fijo: elige otro nombre" } diff --git a/crates/ui-egui/locales/ja-formats.json b/crates/ui-egui/locales/ja-formats.json index cab18a74a..cadc6834f 100644 --- a/crates/ui-egui/locales/ja-formats.json +++ b/crates/ui-egui/locales/ja-formats.json @@ -215,5 +215,6 @@ "There is a “{name}” at the top level already. Merge the two?": "最上位にはすでに「{name}」があります。統合しますか?", "Added “{keyword}” to {n} photo{}": "写真{n}枚に「{keyword}」を追加しました{:.0}", "On {have} of {of} selected photos": "選択した写真{of}枚のうち{have}枚", - "Remove “{name}”": "「{name}」を削除" + "Remove “{name}”": "「{name}」を削除", + "There is a keyword set “{name}” already": "キーワードセット「{name}」はすでにあります" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index ad1b17a3f..3c9a6cf5a 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1764,5 +1764,7 @@ "Edit Set…": "セットを編集…", "Edit Keyword Set": "キーワードセットを編集", "Keywords (⌥1–⌥9)": "キーワード (⌥1–⌥9)", - "A keyword set needs a name": "キーワードセットには名前が必要です" + "A keyword set needs a name": "キーワードセットには名前が必要です", + "Save as a new set": "新しいセットとして保存", + "Recent Keywords is built in: choose another name": "「最近使用したキーワード」は組み込みです。別の名前を選んでください" } diff --git a/crates/ui-egui/locales/pt-br-formats.json b/crates/ui-egui/locales/pt-br-formats.json index 5215af8b3..f0bbcb2c8 100644 --- a/crates/ui-egui/locales/pt-br-formats.json +++ b/crates/ui-egui/locales/pt-br-formats.json @@ -215,5 +215,6 @@ "There is a “{name}” at the top level already. Merge the two?": "Já existe “{name}” no nível superior. Mesclar as duas?", "Added “{keyword}” to {n} photo{}": "“{keyword}” adicionada a {n} foto{}", "On {have} of {of} selected photos": "Em {have} de {of} fotos selecionadas", - "Remove “{name}”": "Remover “{name}”" + "Remove “{name}”": "Remover “{name}”", + "There is a keyword set “{name}” already": "Já existe um conjunto de palavras-chave “{name}”" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 4e4f43847..e2c3f5827 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1764,5 +1764,7 @@ "Edit Set…": "Editar conjunto…", "Edit Keyword Set": "Editar conjunto de palavras-chave", "Keywords (⌥1–⌥9)": "Palavras-chave (⌥1–⌥9)", - "A keyword set needs a name": "Um conjunto de palavras-chave precisa de um nome" + "A keyword set needs a name": "Um conjunto de palavras-chave precisa de um nome", + "Save as a new set": "Salvar como novo conjunto", + "Recent Keywords is built in: choose another name": "“Palavras-chave recentes” é fixo: escolha outro nome" } diff --git a/crates/ui-egui/locales/ru-formats.json b/crates/ui-egui/locales/ru-formats.json index b59e94746..66f9cfd55 100644 --- a/crates/ui-egui/locales/ru-formats.json +++ b/crates/ui-egui/locales/ru-formats.json @@ -215,5 +215,6 @@ "There is a “{name}” at the top level already. Merge the two?": "На верхнем уровне уже есть «{name}». Объединить их?", "Added “{keyword}” to {n} photo{}": "«{keyword}» добавлено к фото ({n}){:.0}", "On {have} of {of} selected photos": "На {have} из {of} выбранных фото", - "Remove “{name}”": "Убрать «{name}»" + "Remove “{name}”": "Убрать «{name}»", + "There is a keyword set “{name}” already": "Набор ключевых слов «{name}» уже есть" } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 49e7c8cea..40cae6c3b 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1439,5 +1439,8 @@ "Edit Keyword Set": "Изменить набор ключевых слов", "Keywords (⌥1–⌥9)": "Ключевые слова (⌥1–⌥9)", "A keyword set needs a name": "Набору ключевых слов нужно название", - "Set name": "Название набора" + "Set name": "Название набора", + "Save as a new set": "Сохранить как новый набор", + "Recent Keywords is built in: choose another name": "«Недавние ключевые слова» — встроенный набор: выберите другое название", + "Recent Keywords": "Недавние ключевые слова" } diff --git a/crates/ui-egui/locales/zh-hans-formats.json b/crates/ui-egui/locales/zh-hans-formats.json index 3d32f0968..b329eef6c 100644 --- a/crates/ui-egui/locales/zh-hans-formats.json +++ b/crates/ui-egui/locales/zh-hans-formats.json @@ -215,5 +215,6 @@ "There is a “{name}” at the top level already. Merge the two?": "顶层已有“{name}”。要合并两者吗?", "Added “{keyword}” to {n} photo{}": "已将“{keyword}”添加到 {n} 张照片{:.0}", "On {have} of {of} selected photos": "所选 {of} 张照片中的 {have} 张", - "Remove “{name}”": "移除“{name}”" + "Remove “{name}”": "移除“{name}”", + "There is a keyword set “{name}” already": "已有关键字集“{name}”" } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 7b4e3c7fb..ba429c36c 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1764,5 +1764,7 @@ "Edit Set…": "编辑集…", "Edit Keyword Set": "编辑关键字集", "Keywords (⌥1–⌥9)": "关键字 (⌥1–⌥9)", - "A keyword set needs a name": "关键字集需要名称" + "A keyword set needs a name": "关键字集需要名称", + "Save as a new set": "另存为新关键字集", + "Recent Keywords is built in: choose another name": "“最近使用的关键字”为内置集:请另选名称" } diff --git a/crates/ui-egui/locales/zh-hant-formats.json b/crates/ui-egui/locales/zh-hant-formats.json index dfe74be2a..ba8462202 100644 --- a/crates/ui-egui/locales/zh-hant-formats.json +++ b/crates/ui-egui/locales/zh-hant-formats.json @@ -215,5 +215,6 @@ "There is a “{name}” at the top level already. Merge the two?": "最上層已有「{name}」。要合併兩者嗎?", "Added “{keyword}” to {n} photo{}": "已將「{keyword}」加入 {n} 張照片{:.0}", "On {have} of {of} selected photos": "選取的 {of} 張照片中的 {have} 張", - "Remove “{name}”": "移除「{name}」" + "Remove “{name}”": "移除「{name}」", + "There is a keyword set “{name}” already": "已有關鍵字集「{name}」" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 274c6cf99..e1011b324 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1764,5 +1764,7 @@ "Edit Set…": "編輯集…", "Edit Keyword Set": "編輯關鍵字集", "Keywords (⌥1–⌥9)": "關鍵字 (⌥1–⌥9)", - "A keyword set needs a name": "關鍵字集需要名稱" + "A keyword set needs a name": "關鍵字集需要名稱", + "Save as a new set": "另存為新關鍵字集", + "Recent Keywords is built in: choose another name": "「最近使用的關鍵字」為內建集:請另選名稱" } diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index b5f8096d8..08788cbba 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -773,7 +773,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { crate::widgets::register(ui.ctx(), id, r.rect); } } - Dialog::KeywordSet { name, slots, .. } => { + Dialog::KeywordSet { replaces, name, slots, as_new } => { ui.label(egui::RichText::new(crate::i18n::tr("Set name")).color(t.text_dim)); let r = crate::text_field::TextField::singleline("field:keywordSetName", name) .hint(crate::i18n::tr("Set name")) @@ -801,6 +801,11 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { } } }); + if replaces.is_some() { + ui.add_space(6.0); + let r = ui.checkbox(as_new, crate::i18n::tr("Save as a new set")); + crate::widgets::register(ui.ctx(), "check:keywordSetAsNew", r.rect); + } // Return in any field saves the set if returned { confirm = true; @@ -1225,13 +1230,26 @@ pub fn confirm_dialog(app: &mut LightcraftApp, dlg: &Dialog) -> Result app.run("keyword.delete", json!({"keyword": keyword})), - Dialog::KeywordSet { replaces, name, slots } => { - if name.trim().is_empty() { + Dialog::KeywordSet { replaces, name, slots, as_new } => { + use lightcraft_catalog::keywords::same; + let name = name.trim(); + if name.is_empty() { return Err(crate::i18n::tr("A keyword set needs a name").to_string()); } - let mut params = json!({"name": name.trim(), "keywords": slots}); - if let Some(old) = replaces { - params["replace"] = json!(old); + if same(name, lightcraft_engine::cmd::keywords::RECENT) { + return Err(crate::i18n::tr("Recent Keywords is built in: choose another name").to_string()); + } + // the set it renames: none when saving as new, or when it was deleted meanwhile (then it is + // saved anew rather than refused) + let sets = &app.session.keyword_sets; + let renames = replaces.as_deref().filter(|_| !as_new).filter(|old| sets.iter().any(|x| same(&x.name, old))); + if sets.iter().any(|x| same(&x.name, name) && !renames.is_some_and(|old| same(&x.name, old))) { + return Err(crate::i18n::tr_format!("There is a keyword set “{name}” already", name = name)); + } + let mut params = json!({"name": name, "keywords": slots}); + match renames { + Some(old) => params["replace"] = json!(old), + None => params["new"] = json!(true), } app.run("keyword.saveSet", params) } diff --git a/crates/ui-egui/src/panels/keyword_list.rs b/crates/ui-egui/src/panels/keyword_list.rs index 940f08ddd..e315d1bda 100644 --- a/crates/ui-egui/src/panels/keyword_list.rs +++ b/crates/ui-egui/src/panels/keyword_list.rs @@ -602,6 +602,9 @@ mod tests { "Edit Set…", "Edit Keyword Set", "A keyword set needs a name", + "Recent Keywords", + "Keywords (⌥1–⌥9)", + "Save as a new set", ]; let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("locales"); for code in ["de", "es", "pt-br", "ru", "ja", "zh-hans", "zh-hant"] { diff --git a/crates/ui-egui/src/panels/right.rs b/crates/ui-egui/src/panels/right.rs index eb26a7f04..01be66fe6 100644 --- a/crates/ui-egui/src/panels/right.rs +++ b/crates/ui-egui/src/panels/right.rs @@ -889,6 +889,7 @@ fn keyword_set(app: &mut LightcraftApp, ui: &mut egui::Ui) { replaces: named.then(|| current.clone()), name: if named { current.clone() } else { String::new() }, slots, + as_new: false, }); } if ui.button(crate::i18n::tr("Save Current Keywords as Set…")).clicked() { diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 86429f1e9..15bc6221f 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -603,6 +603,9 @@ pub enum Dialog { replaces: Option, name: String, slots: Vec, + /// Save as a new set, leaving the edited one (Lightroom Classic's Save as New Preset). + #[serde(default)] + as_new: bool, }, /// Move a keyword inside `parent` (`None`: the top level) where one of its name is already: /// asks before merging the two. diff --git a/crates/ui-egui/src/tests_keyword_set.rs b/crates/ui-egui/src/tests_keyword_set.rs index f62bf65ea..4667f5367 100644 --- a/crates/ui-egui/src/tests_keyword_set.rs +++ b/crates/ui-egui/src/tests_keyword_set.rs @@ -49,7 +49,7 @@ fn editing_recent_keywords_saves_them_as_a_set() { let mut h = keywords_panel(); let recent = sets(&mut h)["keywords"].clone(); open_editor(&mut h); - let Some(crate::state::Dialog::KeywordSet { replaces, name, slots }) = h.app.ui.dialog.clone() else { panic!("{:?}", h.app.ui.dialog) }; + let Some(crate::state::Dialog::KeywordSet { replaces, name, slots, .. }) = h.app.ui.dialog.clone() else { panic!("{:?}", h.app.ui.dialog) }; assert_eq!((replaces, name.as_str()), (None, "")); assert_eq!(json!(slots.iter().filter(|k| !k.is_empty()).collect::>()), recent, "the current nine"); ask(&mut h, "ui.text", json!({"text": "Beach Weddings"})); @@ -111,3 +111,52 @@ fn the_slots_are_alike() { assert!((r.width() - slots[0].width()).abs() < 1.0, "{r:?} vs {:?}", slots[0]); } } + +/// Naming Recent Keywords like a set that exists says so, in the user's words, and keeps the +/// dialog (and that set) as they were. +#[test] +fn a_taken_name_says_so() { + let mut h = keywords_panel(); + h.app.session.execute("keyword.saveSet", &json!({"name": "Travel", "keywords": ["harbour"]})).unwrap(); + h.app.session.execute("keyword.useSet", &json!({"name": "Recent Keywords"})).unwrap(); + h.settle(SETTLE); + open_editor(&mut h); + ask(&mut h, "ui.text", json!({"text": "travel"})); + ask(&mut h, "ui.clickWidget", json!({"id": "button:dialogOk"})); + assert!(matches!(h.app.ui.dialog, Some(crate::state::Dialog::KeywordSet { .. })), "still open"); + let toast = h.app.ui.toast.clone().map(|t| t.0).unwrap_or_default(); + assert!(toast.starts_with("There is a keyword set"), "{toast}"); + let s = sets(&mut h); + let travel = s["sets"].as_array().unwrap().iter().find(|x| x["name"] == "Travel").cloned().unwrap(); + assert_eq!(travel["keywords"], json!(["harbour"]), "left alone"); +} + +/// A set deleted while its dialog was open is saved again, as a new set, rather than refused. +#[test] +fn a_set_deleted_meanwhile_is_saved_anew() { + let mut h = keywords_panel(); + h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony"]})).unwrap(); + h.settle(SETTLE); + open_editor(&mut h); + h.app.session.execute("keyword.deleteSet", &json!({"name": "Weddings"})).unwrap(); + ask(&mut h, "ui.clickWidget", json!({"id": "button:dialogOk"})); + assert_eq!(h.app.ui.dialog, None); + assert_eq!(sets(&mut h)["current"], "Weddings"); +} + +/// Save as a new set (Lightroom Classic's Save as New Preset): the edited set stays, and the new +/// one, under its own name, becomes current. +#[test] +fn save_as_a_new_set_keeps_the_original() { + let mut h = keywords_panel(); + h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony"]})).unwrap(); + h.settle(SETTLE); + open_editor(&mut h); + ask(&mut h, "ui.text", json!({"text": "Ceremonies"})); + ask(&mut h, "ui.clickWidget", json!({"id": "check:keywordSetAsNew"})); + ask(&mut h, "ui.clickWidget", json!({"id": "button:dialogOk"})); + let s = sets(&mut h); + let names: Vec<&str> = s["sets"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect(); + assert_eq!(names, ["Recent Keywords", "Weddings", "Ceremonies"]); + assert_eq!(s["current"], "Ceremonies"); +}