mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-12 00:49:14 -06:00
Record one action call when playback pauses for a step dialog
Fixes #7365 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
3a896a5efc
commit
48009ce53d
@@ -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<String> {
|
||||
|
||||
@@ -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<Value, String> {
|
||||
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<Value>) -> Result<Value, String> {
|
||||
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<Value, Str
|
||||
}
|
||||
}
|
||||
}
|
||||
actions_cmds::finish_playback(&mut app.session, recording, &action, from);
|
||||
actions_cmds::finish_playback(&mut app.session, None, &action, from);
|
||||
match paused.as_ref().and_then(|p| p.get("dialog")).and_then(Value::as_u64).and_then(|d| app.ui.dialog_mut(d)) {
|
||||
// Paused for a dialog: the call goes on after it, so recording stays suspended.
|
||||
Some(d) if failed.is_none() => 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<Str
|
||||
/// The key a paused playback leaves in its dialog's fields: `{action, from}`.
|
||||
pub const RESUME: &str = "__resumeAction";
|
||||
|
||||
/// The recording a paused call suspended, kept in its `RESUME` entry:
|
||||
/// `{index, into, action, from}` (`into` is the name of the action being recorded).
|
||||
/// A step dialog only closes through `dialogs::confirm` (OK, `↩`, automation) or
|
||||
/// `dialogs::cancel` (Cancel, Esc, the close box, automation), which end the call through
|
||||
/// `resume`, `abandoned` or `stopped`, so the recording comes back exactly once.
|
||||
const RECORD: &str = "record";
|
||||
|
||||
/// Resume the recording a paused call suspended and journal that call, once.
|
||||
fn restore_recording(app: &mut PhotocraftApp, owner: Option<&Value>) {
|
||||
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<String, Value>) {
|
||||
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<String, Value>) -> 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<String, Value>)
|
||||
/// A step dialog cancelled: the action stops there, as in Photoshop.
|
||||
pub fn stopped(app: &mut PhotocraftApp, fields: &serde_json::Map<String, Value>) {
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>(), "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};
|
||||
|
||||
@@ -644,6 +644,7 @@ pub fn confirm(app: &mut PhotocraftApp, id: u64) -> Result<Value, String> {
|
||||
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<Value, String> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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 Επιβεβαιώστε ή ακυρώστε πρώτα το ανοιχτό παράθυρο βήματος της ενέργειας
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1030 and column 18.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2653 and column 53.
|
@@ -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 先に開いているアクションのステップのダイアログを確定またはキャンセルしてください
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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 먼저 열려 있는 액션 단계 대화 상자를 확인하거나 취소하세요
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3167 and column 53.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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 Сначала подтвердите или отмените открытый диалог шага операции
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -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 Спочатку підтвердьте або скасуйте відкритий діалог кроку операції
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1388 and column 18.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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 请先确认或取消打开的动作步骤对话框
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2601 and column 53.
|
@@ -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 請先確認或取消開啟中的動作步驟對話框
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user