diff --git a/crates/ui-egui/src/delete_layer_prompt.rs b/crates/ui-egui/src/delete_layer_prompt.rs new file mode 100644 index 00000000..7c1913db --- /dev/null +++ b/crates/ui-egui/src/delete_layer_prompt.rs @@ -0,0 +1,177 @@ +//! Confirmation for a Layers-panel trash-button click; other deletion routes stay immediate. +use crate::{PhotocraftApp, state::DialogKind}; +use serde_json::{Map, Value, json}; +const MARK: &str = "__deleteLayer"; +const PREF: &str = "layer.delete.confirmation"; + +pub fn owns(fields: &Map) -> bool { + fields.contains_key(MARK) +} + +pub fn intercept(app: &mut PhotocraftApp, command: &str, params: &Value) -> Option> { + if app.automation_input + || command != "layer.delete" + || params.get("__trash").and_then(Value::as_bool) != Some(true) + || app.session.prefs().dialogs.get(PREF).and_then(|v| v.get("skip")).and_then(Value::as_bool) == Some(true) + { + return None; + } + let state = app.session.active()?; + let selected = state.selected_layers(); + let ids = if let Some(id) = params.get("layer").and_then(Value::as_u64) { vec![photocraft_doc::LayerId(id)] } else { selected.clone() }; + if ids.is_empty() { + return None; + } + let names = ids.iter().filter_map(|id| state.doc.layer(*id).map(|layer| layer.name.as_str())).take(4).collect::>().join(", "); + let message = if ids.len() == 1 { + crate::i18n::fmt(tl!("Delete layer ‘{name}’?"), &[("name", &names)]) + } else { + crate::i18n::fmt(tl!("Delete {count} layers? {names}"), &[("count", &ids.len().to_string()), ("names", &names)]) + }; + let fields = json!({MARK: true, "__command": command, "__label": "Delete Layer", "message": message, + "document": state.doc.id.0, "selected": selected.iter().map(|id| id.0).collect::>(), "params": params, "skip": false}); + if app.ui.dialogs.iter().any(|d| owns(&d.fields)) { + return Some(Ok(json!({"confirmationPending": true}))); + } + let id = app.ui.open_dialog(DialogKind::Command, fields.as_object().cloned().unwrap_or_default()); + Some(Ok(json!({"dialog": id, "confirmationPending": true}))) +} + +pub fn body(ui: &mut egui::Ui, fields: &mut Map) { + ui.label(fields.get("message").and_then(Value::as_str).unwrap_or(tl!("Delete layer?"))); + ui.add_space(8.0); + let mut skip = fields.get("skip").and_then(Value::as_bool).unwrap_or(false); + ui.checkbox(&mut skip, tl!("Don't show again")); + fields.insert("skip".into(), json!(skip)); +} + +pub fn confirm(app: &mut PhotocraftApp, fields: &Map) -> Result { + let state = app.session.active().ok_or(tl!("The document is closed"))?; + if fields.get("document").and_then(Value::as_u64) != Some(state.doc.id.0) + || fields.get("selected") != Some(&json!(state.selected_layers().iter().map(|id| id.0).collect::>())) + { + return Err(tl!("The document or selected layers changed. Please request deletion again.").into()); + } + let command = fields.get("__command").and_then(Value::as_str).ok_or(tl!("Missing delete command"))?; + if command != "layer.delete" { + return Err(tl!("Invalid delete command").into()); + } + let result = app.run_command(command, fields.get("params").cloned().unwrap_or(Value::Null))?; + if fields.get("skip").and_then(Value::as_bool) == Some(true) { + app.session.edit_prefs(|prefs| { + prefs.dialogs.insert(PREF.into(), json!({"skip": true})); + }); + if let Err(error) = crate::prefs_ui::save_preferences(app) { + app.ui.status = crate::i18n::fmt(tl!("Layer deleted, but confirmation preference could not be saved: {error}"), &[("error", &error)]); + app.ui.status_error = true; + } + } + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + use egui_kittest::kittest::Queryable; + fn make_app() -> PhotocraftApp { + let mut s = photocraft_engine::Session::new(); + s.execute("file.new", json!({"width":8,"height":8})).unwrap(); + s.execute("layer.new.layer", json!({"name":"Ink"})).unwrap(); + PhotocraftApp::new(s, crate::Services::default()) + } + #[test] + fn cancel_keeps_layer_and_confirmation_preference() { + let mut app = make_app(); + let id = app.session.active().unwrap().active_layer.unwrap(); + let result = app.run("layer.delete", json!({"__trash":true})).unwrap(); + let dialog = result["dialog"].as_u64().unwrap(); + assert!(owns(&app.ui.dialogs[0].fields)); + assert!(app.ui.dialogs[0].fields["message"].as_str().unwrap().contains("Ink")); + app.ui.dialog_mut(dialog).unwrap().fields.insert("skip".into(), json!(true)); + app.ui.close_dialog(dialog); + assert!(app.session.active().unwrap().doc.layer(id).is_some()); + assert!(!app.session.prefs().dialogs.contains_key(PREF)); + } + #[test] + fn confirmation_deletes_once_and_remembers_across_restart() { + use std::{cell::RefCell, rc::Rc}; + let mut app = make_app(); + let saved = Rc::new(RefCell::new(String::new())); + let copy = saved.clone(); + app.services.save_prefs = Some(Box::new(move |text| { + *copy.borrow_mut() = text.to_string(); + Ok(()) + })); + let target = app.session.active().unwrap().active_layer.unwrap(); + let before = app.session.active().unwrap().history.past_len(); + let dialog = app.run("layer.delete", json!({"__trash":true})).unwrap()["dialog"].as_u64().unwrap(); + app.ui.dialog_mut(dialog).unwrap().fields.insert("skip".into(), json!(true)); + crate::dialogs::confirm(&mut app, dialog).unwrap(); + assert!(app.session.active().unwrap().doc.layer(target).is_none()); + assert_eq!(app.session.active().unwrap().history.past_len(), before + 1); + assert!(app.ui.dialogs.is_empty()); + app.session.undo(); + assert!(app.session.active().unwrap().doc.layer(target).is_some()); + let text = saved.borrow().clone(); + assert!(text.contains(PREF)); + let mut session = photocraft_engine::Session::new(); + session.execute("file.new", json!({"width":8,"height":8})).unwrap(); + session.execute("layer.new.layer", json!({"name":"Fresh"})).unwrap(); + let mut fresh = PhotocraftApp::new(session, crate::Services { load_prefs: Some(Box::new(move || Some(text.clone()))), ..Default::default() }); + fresh.run("layer.delete", json!({"__trash":true})).unwrap(); + assert!(fresh.ui.dialogs.is_empty()); + } + #[test] + fn changed_document_refuses_confirmation_and_clear_does_not_prompt() { + let mut app = make_app(); + let dialog = app.run("layer.delete", json!({"__trash":true})).unwrap()["dialog"].as_u64().unwrap(); + app.session.execute("file.new", json!({"width":8,"height":8})).unwrap(); + assert!(crate::dialogs::confirm(&mut app, dialog).is_err()); + assert_eq!(app.session.documents()[0].doc.layer_count(), 2); + app.run("edit.clear", json!({})).unwrap(); + assert!(app.ui.dialogs.is_empty()); + } + + #[test] + fn only_trash_click_prompts_and_default_delete_binding_is_unchanged() { + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).with_max_steps(64).build_eframe(|cc| { + PhotocraftApp::setup_context(&cc.egui_ctx, Default::default()); + make_app() + }); + h.run_steps(4); + let id = h.state().session.active().unwrap().active_layer.unwrap(); + h.get_by_label("Delete layer").click(); + h.run_steps(4); + assert!(h.state().ui.dialogs.iter().any(|d| owns(&d.fields))); + assert!(h.state().session.active().unwrap().doc.layer(id).is_some()); + let dialog = h.state().ui.dialogs[0].id; + h.state_mut().ui.close_dialog(dialog); + h.state_mut().run("layer.delete", json!({})).unwrap(); + assert!(h.state().ui.dialogs.is_empty()); + assert!(h.state().session.active().unwrap().doc.layer(id).is_none()); + let clear = photocraft_engine::commands::find("edit.clear").unwrap(); + let delete = photocraft_engine::commands::find("layer.delete").unwrap(); + assert_eq!(clear.shortcut, Some("Delete")); + assert_eq!(delete.shortcut, None); + } + + #[test] + fn confirmation_strings_are_translated_in_every_supported_catalog() { + for language in crate::i18n::LANGUAGES.iter().filter(|l| l.code != "en") { + let lang = crate::i18n::Lang::from_code(language.code).unwrap(); + for key in [ + "Delete layer ‘{name}’?", + "Delete {count} layers? {names}", + "Delete layer?", + "Don't show again", + "The document is closed", + "The document or selected layers changed. Please request deletion again.", + "Missing delete command", + "Invalid delete command", + "Layer deleted, but confirmation preference could not be saved: {error}", + ] { + assert_ne!(crate::i18n::tr(lang, key), key, "{}: {key}", language.code); + } + } + } +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 31459d37..dc64c252 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -190,6 +190,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) { DialogKind::Command if crate::fill_ui::owns(&fields) => crate::fill_ui::body(app, ui, &mut fields), DialogKind::Command if crate::stroke_ui::owns(&fields) => crate::stroke_ui::body(ui, &mut fields), DialogKind::Command if crate::shape_dialog::owns(&fields) => crate::shape_dialog::body(app, ui, &mut fields), + DialogKind::Command if crate::delete_layer_prompt::owns(&fields) => crate::delete_layer_prompt::body(ui, &mut fields), DialogKind::Command if crate::rasterize_prompt::owns(&fields) => crate::rasterize_prompt::body(ui, &fields), DialogKind::Command if crate::variables_ui::owns(&fields) => crate::variables_ui::body(app, ui, &mut fields), DialogKind::Command if crate::file_ui::owns(&fields) => crate::file_ui::body(app, ui, &mut fields), @@ -245,6 +246,8 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) { } else { let ok_label = if d.kind == DialogKind::NewDocument { tl!("Create") + } else if crate::delete_layer_prompt::owns(&d.fields) { + tl!("Delete") } else if d.fields.contains_key("__export") { tl!("Export") } else { @@ -407,6 +410,7 @@ pub fn confirm(app: &mut PhotocraftApp, id: u64) -> Result { DialogKind::Command if crate::fill_ui::owns(&d.fields) => crate::fill_ui::confirm(app, &d.fields), DialogKind::Command if crate::stroke_ui::owns(&d.fields) => crate::stroke_ui::confirm(app, &d.fields), DialogKind::Command if crate::shape_dialog::owns(&d.fields) => crate::shape_dialog::confirm(app, &d.fields), + DialogKind::Command if crate::delete_layer_prompt::owns(&d.fields) => crate::delete_layer_prompt::confirm(app, &d.fields), DialogKind::Command if crate::rasterize_prompt::owns(&d.fields) => crate::rasterize_prompt::confirm(app, &d.fields), DialogKind::Command if crate::variables_ui::owns(&d.fields) => crate::variables_ui::confirm(app, &d.fields), DialogKind::Command if crate::file_ui::owns(&d.fields) => crate::file_ui::confirm(app, &d.fields), diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 9b9b2df7..4e72a3e9 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -963,6 +963,17 @@ Delete current channel Odstranit aktuální kanál Delete current state Odstranit aktuální stav Delete effect layer Odstranit vrstvu efektu + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Smazat vrstvu „{name}“? + Delete {count} layers? {names} Smazat vrstvy ({count})? {names} + Delete layer? Smazat vrstvu? + The document is closed Dokument je zavřený + The document or selected layers changed. Please request deletion again. Dokument nebo vybrané vrstvy se změnily. Požádejte o smazání znovu. + Missing delete command Chybí příkaz ke smazání + Invalid delete command Neplatný příkaz ke smazání + Layer deleted, but confirmation preference could not be saved: {error} Vrstva byla smazána, ale nastavení potvrzení se nepodařilo uložit: {error} + Delete layer Odstranit vrstvu Delete layer comp Odstranit kompozici vrstev Delete note Odstranit poznámku diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 95ce96f2..1a99b2cb 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -947,6 +947,17 @@ Delete current channel Aktuellen Kanal löschen Delete current state Aktuellen Status löschen Delete effect layer Effektebene löschen + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Ebene „{name}“ löschen? + Delete {count} layers? {names} {count} Ebenen löschen? {names} + Delete layer? Ebene löschen? + The document is closed Das Dokument ist geschlossen + The document or selected layers changed. Please request deletion again. Das Dokument oder die ausgewählten Ebenen haben sich geändert. Bitte die Löschung erneut anfordern. + Missing delete command Löschbefehl fehlt + Invalid delete command Ungültiger Löschbefehl + Layer deleted, but confirmation preference could not be saved: {error} Die Ebene wurde gelöscht, aber die Bestätigungseinstellung konnte nicht gespeichert werden: {error} + Delete layer Ebene löschen Delete layer comp Ebenenkomposition löschen Delete note Notiz löschen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index df1adf39..8032a2c9 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2188,6 +2188,17 @@ Delete Pins Διαγραφή καρφιτσών Delete Workspace Διαγραφή χώρου εργασίας Delete Workspace… Διαγραφή χώρου εργασίας… + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Διαγραφή του επιπέδου «{name}»; + Delete {count} layers? {names} Διαγραφή {count} επιπέδων; {names} + Delete layer? Διαγραφή επιπέδου; + The document is closed Το έγγραφο είναι κλειστό + The document or selected layers changed. Please request deletion again. Το έγγραφο ή τα επιλεγμένα επίπεδα άλλαξαν. Ζητήστε ξανά τη διαγραφή. + Missing delete command Λείπει η εντολή διαγραφής + Invalid delete command Μη έγκυρη εντολή διαγραφής + Layer deleted, but confirmation preference could not be saved: {error} Το επίπεδο διαγράφηκε, αλλά δεν ήταν δυνατή η αποθήκευση της προτίμησης επιβεβαίωσης: {error} + Delete layer Διαγραφή επιπέδου Density: Πυκνότητα: Depth Βάθος diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 4df904cf..391b7279 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -948,6 +948,17 @@ Delete current channel Eliminar el canal actual Delete current state Eliminar el estado actual Delete effect layer Eliminar capa de efecto + +# Layers-panel trash confirmation + Delete layer ‘{name}’? ¿Eliminar la capa «{name}»? + Delete {count} layers? {names} ¿Eliminar {count} capas? {names} + Delete layer? ¿Eliminar capa? + The document is closed El documento está cerrado + The document or selected layers changed. Please request deletion again. El documento o las capas seleccionadas han cambiado. Solicita la eliminación de nuevo. + Missing delete command Falta el comando de eliminación + Invalid delete command Comando de eliminación no válido + Layer deleted, but confirmation preference could not be saved: {error} La capa se eliminó, pero no se pudo guardar la preferencia de confirmación: {error} + Delete layer Eliminar capa Delete layer comp Eliminar composición de capa Delete note Eliminar nota diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index d4ec566c..a103f74d 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -938,6 +938,17 @@ Delete current channel Supprimer le canal actif Delete current state Supprimer l'état actuel Delete effect layer Supprimer le calque d'effet + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Supprimer le calque « {name} » ? + Delete {count} layers? {names} Supprimer {count} calques ? {names} + Delete layer? Supprimer le calque ? + The document is closed Le document est fermé + The document or selected layers changed. Please request deletion again. Le document ou les calques sélectionnés ont changé. Demandez à nouveau la suppression. + Missing delete command Commande de suppression manquante + Invalid delete command Commande de suppression non valide + Layer deleted, but confirmation preference could not be saved: {error} Le calque a été supprimé, mais la préférence de confirmation n’a pas pu être enregistrée : {error} + Delete layer Supprimer le calque Delete layer comp Supprimer la composition de calques Delete note Supprimer la note diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 1c7f003d..99284ae5 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -932,6 +932,17 @@ Delete current channel Hapus channel saat ini Delete current state Hapus kondisi saat ini Delete effect layer Hapus layer efek + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Hapus layer ‘{name}’? + Delete {count} layers? {names} Hapus {count} layer? {names} + Delete layer? Hapus layer? + The document is closed Dokumen sudah ditutup + The document or selected layers changed. Please request deletion again. Dokumen atau layer yang dipilih telah berubah. Minta penghapusan lagi. + Missing delete command Perintah hapus tidak tersedia + Invalid delete command Perintah hapus tidak valid + Layer deleted, but confirmation preference could not be saved: {error} Layer dihapus, tetapi preferensi konfirmasi tidak dapat disimpan: {error} + Delete layer Hapus layer Delete layer comp Hapus layer comp Delete note Hapus catatan diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 409b4039..a99a5ea0 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -949,6 +949,17 @@ Delete current channel Elimina canale corrente Delete current state Elimina stato corrente Delete effect layer Elimina livello effetto + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Eliminare il livello «{name}»? + Delete {count} layers? {names} Eliminare {count} livelli? {names} + Delete layer? Eliminare il livello? + The document is closed Il documento è chiuso + The document or selected layers changed. Please request deletion again. Il documento o i livelli selezionati sono cambiati. Richiedi nuovamente l’eliminazione. + Missing delete command Comando di eliminazione mancante + Invalid delete command Comando di eliminazione non valido + Layer deleted, but confirmation preference could not be saved: {error} Il livello è stato eliminato, ma non è stato possibile salvare la preferenza di conferma: {error} + Delete layer Elimina livello Delete layer comp Elimina composizione livelli Delete note Elimina nota diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 0185b828..aa60617f 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -953,6 +953,17 @@ Delete current channel 現在のチャンネルを削除 Delete current state 現在の状態を削除 Delete effect layer 効果レイヤーを削除 + +# Layers-panel trash confirmation + Delete layer ‘{name}’? レイヤー「{name}」を削除しますか? + Delete {count} layers? {names} {count} 個のレイヤーを削除しますか? {names} + Delete layer? レイヤーを削除しますか? + The document is closed ドキュメントは閉じられています + The document or selected layers changed. Please request deletion again. ドキュメントまたは選択したレイヤーが変更されました。もう一度削除してください。 + Missing delete command 削除コマンドがありません + Invalid delete command 無効な削除コマンドです + Layer deleted, but confirmation preference could not be saved: {error} レイヤーは削除されましたが、確認設定を保存できませんでした: {error} + Delete layer レイヤーを削除 Delete layer comp レイヤーカンプを削除 Delete note 注釈を削除 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 5e68a1d3..b26207b0 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -930,6 +930,17 @@ Delete current channel 현재 채널 삭제 Delete current state 현재 상태 삭제 Delete effect layer 효과 레이어 삭제 + +# Layers-panel trash confirmation + Delete layer ‘{name}’? 레이어 ‘{name}’을(를) 삭제하시겠습니까? + Delete {count} layers? {names} 레이어 {count}개를 삭제하시겠습니까? {names} + Delete layer? 레이어를 삭제하시겠습니까? + The document is closed 문서가 닫혔습니다 + The document or selected layers changed. Please request deletion again. 문서 또는 선택한 레이어가 변경되었습니다. 삭제를 다시 요청하세요. + Missing delete command 삭제 명령이 없습니다 + Invalid delete command 잘못된 삭제 명령입니다 + Layer deleted, but confirmation preference could not be saved: {error} 레이어가 삭제되었지만 확인 설정을 저장할 수 없습니다: {error} + Delete layer 레이어 삭제 Delete layer comp 레이어 구성 요소 삭제 Delete note 메모 삭제 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 5874785e..8c4c5d4f 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -941,6 +941,17 @@ Delete current channel Huidig kanaal verwijderen Delete current state Huidige staat verwijderen Delete effect layer Effectlaag verwijderen + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Laag ‘{name}’ verwijderen? + Delete {count} layers? {names} {count} lagen verwijderen? {names} + Delete layer? Laag verwijderen? + The document is closed Het document is gesloten + The document or selected layers changed. Please request deletion again. Het document of de geselecteerde lagen zijn gewijzigd. Vraag de verwijdering opnieuw aan. + Missing delete command Verwijderopdracht ontbreekt + Invalid delete command Ongeldige verwijderopdracht + Layer deleted, but confirmation preference could not be saved: {error} De laag is verwijderd, maar de bevestigingsvoorkeur kon niet worden opgeslagen: {error} + Delete layer Laag verwijderen Delete layer comp Laagcompositie verwijderen Delete note Notitie verwijderen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 5a1a37c3..78f648f4 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -952,6 +952,17 @@ Delete current channel Usuń bieżący kanał Delete current state Usuń bieżący stan Delete effect layer Usuń warstwę efektu + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Usunąć warstwę „{name}”? + Delete {count} layers? {names} Usunąć warstwy ({count})? {names} + Delete layer? Usunąć warstwę? + The document is closed Dokument jest zamknięty + The document or selected layers changed. Please request deletion again. Dokument lub zaznaczone warstwy uległy zmianie. Ponownie zażądaj usunięcia. + Missing delete command Brak polecenia usuwania + Invalid delete command Nieprawidłowe polecenie usuwania + Layer deleted, but confirmation preference could not be saved: {error} Warstwa została usunięta, ale nie udało się zapisać ustawienia potwierdzenia: {error} + Delete layer Usuń warstwę Delete layer comp Usuń kompozycję warstw Delete note Usuń notatkę diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 42a78635..a1bdc1a3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -937,6 +937,17 @@ Delete current channel Excluir o canal atual Delete current state Excluir o estado atual Delete effect layer Excluir camada de efeito + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Excluir a camada “{name}”? + Delete {count} layers? {names} Excluir {count} camadas? {names} + Delete layer? Excluir camada? + The document is closed O documento está fechado + The document or selected layers changed. Please request deletion again. O documento ou as camadas selecionadas mudaram. Solicite a exclusão novamente. + Missing delete command Comando de exclusão ausente + Invalid delete command Comando de exclusão inválido + Layer deleted, but confirmation preference could not be saved: {error} A camada foi excluída, mas não foi possível salvar a preferência de confirmação: {error} + Delete layer Excluir camada Delete layer comp Excluir composição de camadas Delete note Excluir anotação diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index a8cfb2fb..16748f15 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2644,3 +2644,13 @@ fontWeight Italic Курсив Quick Selection found nothing there Быстрое выделение ничего здесь не нашло Object Selection found nothing there — try a tighter rectangle Выделение объектов ничего здесь не нашло — попробуйте более узкий прямоугольник No subject found — try a rectangle around the object Объект не найден — попробуйте прямоугольник вокруг объекта + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Удалить слой «{name}»? + Delete {count} layers? {names} Удалить слои ({count})? {names} + Delete layer? Удалить слой? + The document is closed Документ закрыт + The document or selected layers changed. Please request deletion again. Документ или выбранные слои изменились. Повторите запрос на удаление. + Missing delete command Отсутствует команда удаления + Invalid delete command Недопустимая команда удаления + Layer deleted, but confirmation preference could not be saved: {error} Слой удалён, но не удалось сохранить настройку подтверждения: {error} diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 5f4ed409..66f0f9d7 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2575,6 +2575,17 @@ fontWeight Italic Курсивний Create new snapshot Створити новий знімок стану Delete Cropped Pixels Видаляти обрізані пікселі Delete current state Видалити поточний стан + +# Layers-panel trash confirmation + Delete layer ‘{name}’? Видалити шар «{name}»? + Delete {count} layers? {names} Видалити шари ({count})? {names} + Delete layer? Видалити шар? + The document is closed Документ закрито + The document or selected layers changed. Please request deletion again. Документ або вибрані шари змінилися. Повторіть запит на видалення. + Missing delete command Відсутня команда видалення + Invalid delete command Недійсна команда видалення + Layer deleted, but confirmation preference could not be saved: {error} Шар видалено, але не вдалося зберегти налаштування підтвердження: {error} + Delete layer Видалити шар Diamond Gradient Ромбічний градієнт Enable airbrush-style build-up effects Увімкнути ефекти накопичення як в аерографі diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 754ccfaf..e677291f 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -953,6 +953,17 @@ Delete current channel 删除当前通道 Delete current state 删除当前状态 Delete effect layer 删除效果图层 + +# Layers-panel trash confirmation + Delete layer ‘{name}’? 删除图层“{name}”? + Delete {count} layers? {names} 删除 {count} 个图层?{names} + Delete layer? 删除图层? + The document is closed 文档已关闭 + The document or selected layers changed. Please request deletion again. 文档或所选图层已更改。请重新请求删除。 + Missing delete command 缺少删除命令 + Invalid delete command 无效的删除命令 + Layer deleted, but confirmation preference could not be saved: {error} 图层已删除,但无法保存确认设置:{error} + Delete layer 删除图层 Delete layer comp 删除图层复合 Delete note 删除注释 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index ebaaf6ee..6e9ef866 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -954,6 +954,17 @@ Delete current channel 刪除目前色版 Delete current state 刪除目前狀態 Delete effect layer 刪除效果圖層 + +# Layers-panel trash confirmation + Delete layer ‘{name}’? 刪除圖層「{name}」? + Delete {count} layers? {names} 刪除 {count} 個圖層?{names} + Delete layer? 刪除圖層? + The document is closed 文件已關閉 + The document or selected layers changed. Please request deletion again. 文件或所選圖層已變更。請重新要求刪除。 + Missing delete command 缺少刪除指令 + Invalid delete command 無效的刪除指令 + Layer deleted, but confirmation preference could not be saved: {error} 圖層已刪除,但無法儲存確認設定:{error} + Delete layer 刪除圖層 Delete layer comp 刪除圖層構圖 Delete note 刪除註解 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 01a19150..64e020e0 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -50,6 +50,7 @@ pub mod crop_overlay; pub mod crop_shield; pub mod crop_straighten; pub mod crop_ui; +pub mod delete_layer_prompt; pub mod dialog_blend_ui; pub mod dialogs; pub mod direct_select; @@ -722,6 +723,9 @@ impl PhotocraftApp { /// Run an engine command, reporting errors in the status bar. pub fn run(&mut self, id: &str, params: Value) -> Result { + if let Some(result) = delete_layer_prompt::intercept(self, id, ¶ms) { + return result; + } // Automation input also gates every step a command runs on its behalf (`actions.play`). let gate = if self.automation_input && self.session.authorize.is_none() { self.services.automation_authorize } else { None }; if gate.is_some() { diff --git a/crates/ui-egui/src/mask_thumbs_ui/tests.rs b/crates/ui-egui/src/mask_thumbs_ui/tests.rs index 4d3b18cb..813c643a 100644 --- a/crates/ui-egui/src/mask_thumbs_ui/tests.rs +++ b/crates/ui-egui/src/mask_thumbs_ui/tests.rs @@ -101,6 +101,7 @@ fn footer_trash_deletes_only_the_targeted_mask_linked_or_unlinked() { let at = h.get_by_label(label).rect().center(); click_with(&mut h, at, Modifiers::NONE); let l = layer(&h, masked); + assert!(h.state().ui.dialogs.is_empty(), "mask deletion remains immediate"); let original = before.layer(photocraft_doc::LayerId(masked)).unwrap(); assert_eq!(l.content, original.content); assert_eq!(layer(&h, shape), *before.layer(photocraft_doc::LayerId(shape)).unwrap()); @@ -130,6 +131,23 @@ fn footer_trash_still_deletes_the_layer_when_its_content_is_targeted() { assert!(!h.state().ui.mask_target && !h.state().ui.vector_mask_target); let at = h.get_by_label("Delete layer").rect().center(); click_with(&mut h, at, Modifiers::NONE); + assert!(h.state().session.active().unwrap().doc.layer(photocraft_doc::LayerId(masked)).is_some()); + assert!(h.state().ui.dialogs.iter().any(|d| crate::delete_layer_prompt::owns(&d.fields))); + h.get_by_label("Delete").click(); + h.run_steps(3); + assert!(h.state().session.active().unwrap().doc.layer(photocraft_doc::LayerId(masked)).is_none()); +} + +/// Like Photoshop, ⌥/Alt-clicking the trash deletes the layer without asking. +#[test] +fn alt_click_on_the_footer_trash_deletes_without_confirmation() { + let (s, masked, _) = session(); + let mut h = harness(s, 0, 1.0, 290.0); + let row = crate::layer_row_ui::recorded(&h.ctx).into_iter().find(|r| r.layer == masked).unwrap().row; + click_with(&mut h, pos2(row.left() + 46.0, row.center().y), Modifiers::NONE); + let at = h.get_by_label("Delete layer").rect().center(); + click_with(&mut h, at, Modifiers::ALT); + assert!(h.state().ui.dialogs.is_empty(), "no confirmation"); assert!(h.state().session.active().unwrap().doc.layer(photocraft_doc::LayerId(masked)).is_none()); } diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index e60db729..131e4fcf 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -1905,7 +1905,10 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) { }; let trash = icons::button(ui, "trash", 26.0, false, tl!(label)); if trash.clicked() { - actions.push((delete.into(), json!({}))); + // Like Photoshop, ⌥/Alt-click deletes without asking. + let confirm = delete == "layer.delete" && !ui.input(|i| i.modifiers.alt); + let params = if confirm { json!({"__trash": true}) } else { json!({}) }; + actions.push((delete.into(), params)); } actions.extend(footer_drop(ui, &trash, footer_drag, "layer.delete")); // A click follows the selected thumbnail; name that action for screen readers even