From 48009ce53d761d3d0b3dc759ff067d7bb3cd0872 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Mon, 19 Oct 2026 06:49:57 +0200 Subject: [PATCH] Record one action call when playback pauses for a step dialog Fixes #7365 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/actions_cmds.rs | 17 ++- crates/ui-egui/src/actions.rs | 225 +++++++++++++++++++++++++++- crates/ui-egui/src/dialogs.rs | 3 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/id.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/nl.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sr.tsv | 2 + crates/ui-egui/src/i18n/tr.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/lib.rs | 1 + 24 files changed, 276 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/actions_cmds.rs b/crates/engine/src/actions_cmds.rs index d881a6112b..97d5110271 100644 --- a/crates/engine/src/actions_cmds.rs +++ b/crates/engine/src/actions_cmds.rs @@ -348,15 +348,20 @@ pub fn finish_playback(s: &mut Session, recording: Option<(usize, usize)>, actio s.actions.playback_stack.pop(); s.actions.playing = s.actions.playing.saturating_sub(1); if let Some((_, idx)) = recording { - s.actions.recording = Some((s.journal.len(), idx)); - let mut params = json!({"action": action.name}); - if from > 0 { - params["from"] = json!(from); - } - s.journal.push(("actions.play".into(), params)); + restore_recording(s, idx, &action.name, from); } } +/// Resume recording into action `idx` and journal the named call that suspended it. +pub fn restore_recording(s: &mut Session, idx: usize, action: &str, from: usize) { + s.actions.recording = Some((s.journal.len(), idx)); + let mut params = json!({"action": action}); + if from > 0 { + params["from"] = json!(from); + } + s.journal.push(("actions.play".into(), params)); +} + /// Nested playback returns a structured failure even though the command itself succeeded. /// Promote it to the caller's failed step so no later parent steps run after a child fails. pub fn nested_failure(id: &str, value: &Value) -> Option { diff --git a/crates/ui-egui/src/actions.rs b/crates/ui-egui/src/actions.rs index 8b52ba88c5..1153e65666 100644 --- a/crates/ui-egui/src/actions.rs +++ b/crates/ui-egui/src/actions.rs @@ -95,8 +95,32 @@ pub(crate) fn assign_shortcut(app: &mut PhotocraftApp, name: &str, shortcut: &st /// Replay edits synchronously, with view steps handled by the shell in the same order. /// Engine steps keep the headless action semantics (no dialogs or background jobs). pub(crate) fn play(app: &mut PhotocraftApp, params: &Value) -> Result { - let (action, from) = actions_cmds::playback_plan(&app.session, params).map_err(|e| e.to_string())?; - let recording = actions_cmds::begin_playback(&mut app.session, &action).map_err(|e| e.to_string())?; + play_owned(app, params, None) +} + +/// `suspended` is the recording a paused part of the same call took (`RECORD`); it comes +/// back, with that named call journaled, only when the whole call ends. +fn play_owned(app: &mut PhotocraftApp, params: &Value, suspended: Option) -> Result { + let (action, from) = match actions_cmds::playback_plan(&app.session, params) { + Ok(plan) => plan, + Err(e) => { + restore_recording(app, suspended.as_ref()); + return Err(e.to_string()); + } + }; + let recording = match actions_cmds::begin_playback(&mut app.session, &action) { + Ok(r) => r, + Err(e) => { + restore_recording(app, suspended.as_ref()); + return Err(e.to_string()); + } + }; + let owner = recording + .map(|(_, idx)| { + let into = app.session.actions.list.get(idx).map(|a| a.name.clone()); + json!({"index": idx, "into": into, "action": action.name, "from": from}) + }) + .or(suspended); let mut ran = 0u64; let mut failed = None; let mut paused = None; @@ -154,7 +178,18 @@ pub(crate) fn play(app: &mut PhotocraftApp, params: &Value) -> Result match (owner, d.fields.get_mut(RESUME)) { + (Some(owner), Some(Value::Object(r))) => { + r.insert(RECORD.into(), owner); + } + // No continuation to carry the recording: the call ends here. + (owner, _) => restore_recording(app, owner.as_ref()), + }, + _ => restore_recording(app, owner.as_ref()), + } Ok(match (failed, paused) { (Some(f), _) => json!({"action": action.name, "ran": ran, "failed": f}), (None, Some(p)) => json!({"action": action.name, "ran": ran, "paused": p}), @@ -434,21 +469,74 @@ pub fn insert_stop_confirm(app: &mut PhotocraftApp, fields: &serde_json::Map) { + let Some(owner) = owner else { return }; + let (Some(idx), Some(into), Some(name)) = ( + owner.get("index").and_then(Value::as_u64).and_then(|n| usize::try_from(n).ok()), + owner.get("into").and_then(Value::as_str), + owner.get("action").and_then(Value::as_str), + ) else { + return; + }; + let from = owner.get("from").and_then(Value::as_u64).and_then(|n| usize::try_from(n).ok()).unwrap_or(0); + // `suspended_guard` refuses what would move the recorded action, but another command may + // still replace the list: the call goes to the action of that name, never to whatever now + // sits at the old index, and is dropped if that action is gone. + let list = &app.session.actions.list; + let target = if list.get(idx).is_some_and(|a| a.name == into) { Some(idx) } else { list.iter().position(|a| a.name == into) }; + if let Some(idx) = target + && app.session.actions.recording.is_none() + { + actions_cmds::restore_recording(&mut app.session, idx, name, from); + } +} + +/// Whether a step dialog holds a suspended recording. +fn suspended(app: &PhotocraftApp) -> bool { + app.ui.dialogs.iter().any(|d| d.fields.get(RESUME).and_then(|r| r.get(RECORD)).is_some()) +} + +/// While a step dialog holds a suspended recording, refuse what would lose its call: a new +/// recording (the restore would find recording taken) or deleting/moving actions (the recorded +/// action could go away or change place). The engine checks these against `recording`, which +/// is `None` then. +pub(crate) fn suspended_guard(app: &PhotocraftApp, id: &str) -> Result<(), String> { + if matches!(id, "actions.record" | "actions.delete" | "actions.move") && suspended(app) { + return Err(tl!("Confirm or cancel the open action step dialog first").into()); + } + Ok(()) +} + +/// A step dialog's command failed: the action ends there. +pub fn abandoned(app: &mut PhotocraftApp, fields: &serde_json::Map) { + restore_recording(app, fields.get(RESUME).and_then(|r| r.get(RECORD))); +} + /// A step dialog confirmed: continue the action after it (`dialogs::confirm` calls this). pub fn resume(app: &mut PhotocraftApp, fields: &serde_json::Map) -> Result<(), String> { let Some(r) = fields.get(RESUME) else { return Ok(()) }; + let owner = r.get(RECORD).cloned(); // The dialog's command may run as a background job; the next step needs its result. if let Some(doc) = app.session.active().map(|d| d.doc.id) && let Some(job) = app.session.job_on(doc) && let Err(e) = app.session.wait_job(job.id) { + restore_recording(app, owner.as_ref()); let template = "Action playback failed: {error}"; app.ui.status = crate::i18n::fmt(tl!(template), &[("error", &e.to_string())]); app.ui.status_error = true; return Err(e.to_string()); } let params = json!({"action": r.get("action").cloned().unwrap_or(Value::Null), "from": r.get("from").cloned().unwrap_or(Value::Null)}); - match play(app, ¶ms) { + match play_owned(app, ¶ms, owner) { Ok(v) => { report_play(app, &v); Ok(()) @@ -465,6 +553,7 @@ pub fn resume(app: &mut PhotocraftApp, fields: &serde_json::Map) /// A step dialog cancelled: the action stops there, as in Photoshop. pub fn stopped(app: &mut PhotocraftApp, fields: &serde_json::Map) { if fields.contains_key(RESUME) { + abandoned(app, fields); app.ui.status = tl!("Action playback stopped").to_string(); app.ui.status_error = false; } @@ -1687,6 +1776,134 @@ mod tests { assert!(v.get("paused").is_none()); } + /// An 8×6 document, a Child action whose first step (a blur) shows its dialog and whose + /// second step adds a "Suffix" layer, and recording into Owner started. + fn recording_with_dialog_child() -> PhotocraftApp { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"width": 8, "height": 6, "background": "#203040"})).unwrap(); + let mut child = + Action::new("Child", vec![("filter.blur.gaussianBlur".into(), json!({"radius": 1.5})), ("layer.new.layer".into(), json!({"name": "Suffix"}))]); + child.dialogs.insert(0); + app.session.actions.list.push(child); + app.run("actions.record", json!({"name": "Owner"})).unwrap(); + app + } + + fn suffixes(app: &PhotocraftApp) -> usize { + app.session.active().unwrap().doc.walk().iter().filter(|(_, _, l)| l.name == "Suffix").count() + } + + fn child_call() -> Vec<(String, Value)> { + vec![("actions.play".to_string(), json!({"action": "Child"}))] + } + + /// Play Child and return the id of its step dialog. + fn pause_child(app: &mut PhotocraftApp) -> u64 { + let v = play(app, &json!({"action": "Child"})).unwrap(); + v["paused"]["dialog"].as_u64().unwrap() + } + + #[test] + fn a_recorded_call_paused_by_a_dialog_records_one_call_and_replays_once() { + let mut app = recording_with_dialog_child(); + let refusal = crate::i18n::t("Confirm or cancel the open action step dialog first").to_string(); + + // Confirm: the dialog command and the resumed rest stay inside the one named call. + let live = app.session.journal.len(); + let id = pause_child(&mut app); + assert!(app.session.actions.recording.is_none(), "recording stays suspended while the step dialog is open"); + let names = |app: &PhotocraftApp| app.session.actions.list.iter().map(|a| a.name.clone()).collect::>(); + let before = names(&app); + assert_eq!(app.run("actions.record", json!({"name": "Intruder"})), Err(refusal.clone()), "no second recording while the call is suspended"); + assert_eq!(app.run("actions.delete", json!({"action": "Owner"})), Err(refusal.clone()), "the suspended owner can't be deleted"); + assert_eq!(app.run("actions.move", json!({"action": "Owner", "to": 0})), Err(refusal.clone()), "the suspended owner can't be moved"); + assert_eq!(names(&app), before, "the refused commands leave the list alone"); + crate::dialogs::confirm(&mut app, id).unwrap(); + assert!(app.session.actions.recording.is_some(), "recording comes back when the call completes"); + app.run("actions.stop", json!({})).unwrap(); + assert_eq!(app.session.actions.list[1].steps, child_call()); + assert_eq!(suffixes(&app), 1); + + // Replaying the recording repeats the call once. + app.run("file.new", json!({"width": 8, "height": 6})).unwrap(); + let start = app.session.journal.len(); + app.automation_input = true; + let v = play(&mut app, &json!({"action": "Owner"})).unwrap(); + app.automation_input = false; + assert!(v.get("failed").is_none(), "{v}"); + assert_eq!(suffixes(&app), 1, "replay creates the suffix once"); + let edits = |from: usize| -> Vec<(String, Value)> { + app.session.journal[from..].iter().filter(|(id, _)| !id.starts_with("actions.") && id != "file.new").cloned().collect() + }; + let expected = vec![("filter.blur.gaussianBlur".to_string(), json!({"radius": 1.5})), ("layer.new.layer".to_string(), json!({"name": "Suffix"}))]; + assert_eq!(edits(start), expected, "replay journals the blur and the suffix once each, in order"); + assert_eq!(edits(live), expected[..].iter().chain(&expected).cloned().collect::>(), "live then replay"); + + // Cancel: the call ends at the dialog and is recorded once. + app.run("actions.record", json!({"name": "Cancelled"})).unwrap(); + let id = pause_child(&mut app); + crate::dialogs::cancel(&mut app, id).unwrap(); + assert!(app.session.actions.recording.is_some(), "cancel restores recording"); + app.run("actions.stop", json!({})).unwrap(); + assert_eq!(app.session.actions.list[2].steps, child_call()); + } + + #[test] + fn a_failing_step_dialog_command_ends_the_recorded_call_once() { + let mut app = recording_with_dialog_child(); + let id = pause_child(&mut app); + assert!(app.session.actions.recording.is_none(), "recording stays suspended while the step dialog is open"); + app.ui.dialog_mut(id).unwrap().fields.insert("radius".into(), json!("not a radius")); + assert!(crate::dialogs::confirm(&mut app, id).is_err(), "the dialog command fails"); + assert!(app.session.actions.recording.is_some(), "recording comes back when the dialog command fails"); + assert_eq!(suffixes(&app), 0, "the action stops at the failed step"); + app.run("actions.stop", json!({})).unwrap(); + assert_eq!(app.session.actions.list[1].steps, child_call()); + } + + #[test] + fn a_failing_resumed_playback_ends_the_recorded_call_once() { + let mut app = recording_with_dialog_child(); + let id = pause_child(&mut app); + // The rest of the call can't be found any more: resuming it fails. + app.session.actions.list[0].name = "Renamed".into(); + assert!(crate::dialogs::confirm(&mut app, id).is_err(), "the resumed playback fails"); + assert!(app.session.actions.recording.is_some(), "recording comes back when the resumed playback fails"); + app.run("actions.stop", json!({})).unwrap(); + assert_eq!(app.session.actions.list[1].steps, child_call()); + } + + #[test] + fn a_suspended_call_is_recorded_into_its_action_after_the_list_changes() { + let mut app = recording_with_dialog_child(); + let id = pause_child(&mut app); + // Something else puts an action where Owner was. + app.session.actions.list.insert(1, Action::new("Bystander", vec![])); + crate::dialogs::confirm(&mut app, id).unwrap(); + app.run("actions.stop", json!({})).unwrap(); + assert!(app.session.actions.list[1].steps.is_empty(), "the action now at the old index gets nothing"); + assert_eq!(app.session.actions.list[2].name, "Owner"); + assert_eq!(app.session.actions.list[2].steps, child_call()); + } + + #[test] + fn escape_on_a_step_dialog_ends_the_recorded_call_once() { + let app = recording_with_dialog_child(); + let mut h = egui_kittest::Harness::builder().with_size(vec2(900.0, 700.0)).build_ui_state(|ui, app| crate::dialogs::show(app, ui.ctx()), app); + PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]); + h.run_steps(2); + pause_child(h.state_mut()); + h.run_steps(4); + assert!(h.state().session.actions.recording.is_none(), "recording stays suspended while the step dialog is open"); + h.key_press(egui::Key::Escape); + h.run_steps(2); + assert!(h.state().ui.dialogs.is_empty(), "Esc closes the step dialog"); + assert!(h.state().session.actions.recording.is_some(), "Esc restores recording"); + let app = h.state_mut(); + app.run("actions.stop", json!({})).unwrap(); + assert_eq!(app.session.actions.list[1].steps, child_call()); + } + #[test] fn button_mode_plays_the_clicked_action_and_hides_its_steps() { use egui_kittest::{Harness, kittest::Queryable}; diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index e27411b7e0..d602164a5e 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -644,6 +644,7 @@ pub fn confirm(app: &mut PhotocraftApp, id: u64) -> Result { DialogKind::Command => { let Some(cmd) = d.fields.get("__command").and_then(Value::as_str).map(str::to_string) else { app.filter_preview = None; + crate::actions::abandoned(app, &d.fields); return Err("dialog has no command".into()); }; let (cmd, params) = crate::smart_ui::confirm_command(&d.fields, cmd, crate::filter_dialog::params_of(&d.fields)); @@ -665,6 +666,8 @@ pub fn confirm(app: &mut PhotocraftApp, id: u64) -> Result { crate::filter_dialog::remember(app, &cmd, &d.fields); // A step dialog of a playing action: the action continues after it. crate::actions::resume(app, &d.fields)?; + } else { + crate::actions::abandoned(app, &d.fields); } result } diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index f6046fc207..e90c36206b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3852,3 +3852,5 @@ navigator Zoom Zvětšení Use Použít In Front Vpředu Behind Vzadu +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Nejprve potvrďte nebo zrušte otevřený dialog kroku akce diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 0ad7366c4b..6150f1c525 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3245,3 +3245,5 @@ curves Smooth Glätten In Front Davor Behind Dahinter Blend Mischen +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Bestätige oder brich zuerst den offenen Dialog des Aktionsschritts ab diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 83621629fe..a08a3b98cd 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3944,3 +3944,5 @@ curves Smooth Εξομάλυνση Use Χρήση In Front Μπροστά Behind Πίσω +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Επιβεβαιώστε ή ακυρώστε πρώτα το ανοιχτό παράθυρο βήματος της ενέργειας diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index e23b4e84c7..20fc7edec3 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4206,3 +4206,5 @@ navigator Zoom Zoom Use Usar In Front Delante Behind Detrás +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Confirme o cancele primero el cuadro de diálogo abierto del paso de la acción diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 3bcca7037c..da54fb1366 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3252,3 +3252,5 @@ curves Smooth Lisser Use Utiliser In Front Devant Behind Derrière +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Validez ou annulez d’abord la boîte de dialogue ouverte de l’étape de l’action diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 4907e2da0c..f4666ee3ff 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4163,3 +4163,5 @@ cameraRaw Yellow Sárga Use Használat In Front Elöl Behind Mögött +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Előbb hagyd jóvá vagy vond vissza a művelet lépésének nyitott párbeszédablakát diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index fc0d66afae..2c86adf2ef 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -3239,3 +3239,5 @@ curves Smooth Haluskan Use Gunakan In Front Di Depan Behind Di Belakang +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Konfirmasi atau batalkan dulu dialog langkah tindakan yang terbuka diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 609469635f..9fec50ccfc 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4361,3 +4361,5 @@ shape Appearance Aspetto Use Usa In Front Davanti Behind Dietro +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Conferma o annulla prima la finestra di dialogo aperta del passo dell’azione diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 39a38a1142..be5d669fe3 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3921,3 +3921,5 @@ curves Smooth スムーズ Use 使用 In Front 前面 Behind 背面 +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first 先に開いているアクションのステップのダイアログを確定またはキャンセルしてください diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 241c81bef5..7985611314 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -3426,3 +3426,5 @@ curves Smooth 매끄럽게 In Front 앞 Behind 뒤 Blend 혼합 +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first 먼저 열려 있는 액션 단계 대화 상자를 확인하거나 취소하세요 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index e0dd17ff57..93f2c9ed01 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -4004,3 +4004,5 @@ curves Smooth Vloeiend maken Use Gebruiken In Front Voor Behind Achter +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Bevestig of annuleer eerst het geopende dialoogvenster van de handelingsstap diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 10435771f5..b23b377028 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3843,3 +3843,5 @@ curves Smooth Wygładź Use Użyj In Front Z przodu Behind Z tyłu +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Najpierw zatwierdź lub anuluj otwarte okno kroku operacji diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index f4dc857f7d..54612e2e61 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4034,3 +4034,5 @@ layerLabel No Color Sem cor Use Usar In Front Na frente Behind Atrás +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Confirme ou cancele primeiro a caixa de diálogo aberta da etapa da ação diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index b497b41855..c64d1d44ba 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3218,3 +3218,5 @@ curves Smooth Сгладить Use Использовать In Front Спереди Behind Сзади +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Сначала подтвердите или отмените открытый диалог шага операции diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index dc3608c514..529126732f 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -4317,3 +4317,5 @@ shape Appearance Izgled Use Koristi In Front Ispred Behind Iza +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Prvo potvrdite ili otkažite otvoreni dijalog koraka radnje diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 5e5326f9f0..d36d8e25a9 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3974,3 +3974,5 @@ cameraData Lens Lens Use Kullan In Front Önde Behind Arkada +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Önce açık eylem adımı iletişim kutusunu onaylayın veya iptal edin diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index cc3df88348..3e61658a18 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3952,3 +3952,5 @@ curves Smooth Згладити Use Використати In Front Спереду Behind Ззаду +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Спочатку підтвердьте або скасуйте відкритий діалог кроку операції diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index d19e3d576a..35bf55459c 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3731,3 +3731,5 @@ curves Smooth Làm mượt Use Dùng In Front Phía trước Behind Phía sau +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first Hãy xác nhận hoặc hủy hộp thoại bước hành động đang mở trước diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 675a544509..6a787f6719 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2868,3 +2868,5 @@ curves Smooth 平滑 In Front 前面 Behind 后面 Blend 混合 +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first 请先确认或取消打开的动作步骤对话框 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 982fa97319..85161923be 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2853,3 +2853,5 @@ curves Smooth 平滑 In Front 前面 Behind 後面 Blend 混合 +# Actions: a step dialog holds a suspended recording (#7365) + Confirm or cancel the open action step dialog first 請先確認或取消開啟中的動作步驟對話框 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 89f293a2cb..0116740707 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1121,6 +1121,7 @@ impl PhotocraftApp { return Err(if self.ui.status.is_empty() { tl!("The transform couldn't be applied.").into() } else { self.ui.status.clone() }); } } + actions::suspended_guard(self, id)?; if let Some(result) = panel_docking::command(self, id, ¶ms) { return result; }