mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 05:27:08 -06:00
Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts: # docs/gaps.md
This commit is contained in:
@@ -7,6 +7,9 @@
|
||||
//! panels appear at the bottom of the Window menu and open as dockable panels
|
||||
//! (`window.scriptPanel`), running with `this` = the panel as in After Effects. A subfolder of
|
||||
//! either folder (one level) is a submenu named after it; its scripts are named `Folder/File.jsx`.
|
||||
//! * **Startup and Shutdown scripts** in `Scripts/Startup/` and `Scripts/Shutdown/` run by
|
||||
//! themselves, in alphabetical order: at launch (`file.runStartupScripts`) and as EffectCraft
|
||||
//! quits, after the project is closed (`file.runShutdownScripts`).
|
||||
//! * **ScriptUI windows** (`scriptui.*`): list the open script windows, read their control
|
||||
//! trees, click buttons and set values like a user, close them (see [`crate::scriptui`]).
|
||||
//! * **Script settings** (`script.settings.*`): After Effects' `app.settings`, string values by
|
||||
@@ -23,6 +26,18 @@ use crate::{EngineError, Event, Result, Session, cmd, scriptui};
|
||||
/// The settings-store folders of installed scripts.
|
||||
pub const SCRIPTS_DIR: &str = "Scripts";
|
||||
pub const PANELS_DIR: &str = "Scripts/ScriptUI Panels";
|
||||
/// The settings-store folders whose scripts run at launch and as EffectCraft quits.
|
||||
pub const STARTUP_DIR: &str = "Scripts/Startup";
|
||||
pub const SHUTDOWN_DIR: &str = "Scripts/Shutdown";
|
||||
/// Commands that run script files or script code (a ScriptUI panel runs its script when it
|
||||
/// opens). Scripts and batches can't run them, and confined automation sessions refuse them.
|
||||
pub const RUNS_SCRIPTS: &[&str] =
|
||||
&["script.run", "file.runScript", "file.runStartupScripts", "file.runShutdownScripts", "window.scriptPanel", "window.restoreScriptPanels"];
|
||||
/// Largest script file the Startup and Shutdown folders run (bytes), as `#include` and
|
||||
/// `$.evalFile` allow.
|
||||
const MAX_SCRIPT_BYTES: usize = 16 << 20;
|
||||
/// Modal dialogs a Shutdown script may open one after another; each closes as if canceled.
|
||||
const MAX_SHUTDOWN_DIALOGS: usize = 64;
|
||||
/// [`PANELS_DIR`]'s name in [`SCRIPTS_DIR`]: it holds the panels, so it is not a folder of scripts.
|
||||
const PANELS_FOLDER: &str = "ScriptUI Panels";
|
||||
/// Subfolders of Scripts that hold scripts the app runs itself (panels; launch and quit, as in
|
||||
@@ -339,6 +354,85 @@ fn restore_panels(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
Ok(json!({"opened": opened, "failed": failed}))
|
||||
}
|
||||
|
||||
/// Run the scripts in `dir` of the settings store one after another, in alphabetical order (case
|
||||
/// ignored), as After Effects runs its Startup and Shutdown folders. A script that fails is
|
||||
/// reported in a toast and the log and listed in `failed`; the next one still runs. A script waiting in a
|
||||
/// modal dialog is listed in `waiting` and goes on when the dialog closes; the next one runs
|
||||
/// meanwhile. With `quitting`, no window is left to show a dialog in, so each dialog closes as
|
||||
/// if canceled (`show()` returns 2) and the script runs on.
|
||||
fn run_folder(s: &mut Session, dir: &str, quitting: bool) -> Value {
|
||||
let mut names: Vec<String> = s.config.as_ref().map(|c| c.list(dir)).unwrap_or_default().into_iter().filter(|n| is_script(n) && is_script_name(n)).collect();
|
||||
names.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()).then_with(|| a.cmp(b)));
|
||||
let (mut ran, mut waiting, mut failed) = (vec![], vec![], vec![]);
|
||||
for name in names {
|
||||
let code = match s.config.as_ref().map(|c| c.read_bounded(&format!("{dir}/{name}"), MAX_SCRIPT_BYTES)) {
|
||||
Some(Ok(Some(code))) => code,
|
||||
read => {
|
||||
let why = match read {
|
||||
Some(Err(e)) => format!(": {e}"),
|
||||
_ => String::new(),
|
||||
};
|
||||
let error = format!("{name}: cannot read the script{why}");
|
||||
log::warn!("{dir}: {error}");
|
||||
s.events.push(Event::Toast { message: error.clone(), error: true });
|
||||
failed.push(json!({"name": name, "error": error}));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let is_waiting = |r: &Result<Value>| r.as_ref().is_ok_and(|v| v.get("waiting").and_then(Value::as_bool) == Some(true));
|
||||
// In a settings folder on disk, the script runs as its file, so `#include`, `$.fileName`
|
||||
// and `$.evalFile` find the files beside it.
|
||||
let path = s.config.as_ref().and_then(|c| c.dir()).map(|d| d.join(dir).join(&name).to_string_lossy().into_owned()).unwrap_or_else(|| name.clone());
|
||||
// Windows open before this script runs belong to other scripts, perhaps of the same name.
|
||||
let before: Vec<u32> = s.script_ui.windows.iter().map(|w| w.id).collect();
|
||||
let hosts_before = s.script_ui.task_hosts.clone();
|
||||
let mut r = super::file_more::run_js_file(s, &code, &path, &name);
|
||||
if !quitting {
|
||||
// The Startup script's context: the one that scheduled tasks or holds the windows it
|
||||
// opened (a task scheduled after its dialog closes runs there too).
|
||||
let tasks = s.script_ui.task_hosts.iter().filter(|h| !hosts_before.contains(h)).copied();
|
||||
let windows = s.script_ui.windows.iter().filter(|w| !before.contains(&w.id)).map(|w| w.host);
|
||||
let hosts: Vec<u32> = tasks.chain(windows).collect();
|
||||
for h in hosts {
|
||||
if !s.script_ui.startup_hosts.contains(&h) {
|
||||
s.script_ui.startup_hosts.push(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut closed = 0;
|
||||
while quitting && closed < MAX_SHUTDOWN_DIALOGS && is_waiting(&r) {
|
||||
let Some(id) = s.script_ui.windows.iter().rev().find(|w| w.modal && w.script == path && !before.contains(&w.id)).map(|w| w.id) else { break };
|
||||
// report_js reports an error the script throws after its dialog closes, as it does for an
|
||||
// error in the script's first run.
|
||||
r = scriptui::close(s, &json!({"window": id})).and_then(|out| super::file_more::report_js(s, &name, out));
|
||||
closed += 1;
|
||||
}
|
||||
match r {
|
||||
Ok(_) if is_waiting(&r) => waiting.push(name),
|
||||
Ok(_) => ran.push(name),
|
||||
Err(e) => {
|
||||
// Also in the log: no frame draws a Shutdown script's toast.
|
||||
log::warn!("{dir}: {e}");
|
||||
failed.push(json!({"name": name, "error": e.to_string()}));
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({"ran": ran, "waiting": waiting, "failed": failed})
|
||||
}
|
||||
|
||||
/// `file.runStartupScripts`: run the scripts in `Scripts/Startup` (frontends call it once at
|
||||
/// launch, before a project opens).
|
||||
fn run_startup(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
Ok(run_folder(s, STARTUP_DIR, false))
|
||||
}
|
||||
|
||||
/// `file.runShutdownScripts`: close the project, then run the scripts in `Scripts/Shutdown`
|
||||
/// (frontends call it as the app quits, after the unsaved-changes prompt).
|
||||
fn run_shutdown(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
s.replace_project(effectcraft_project::Project::default(), None);
|
||||
Ok(run_folder(s, SHUTDOWN_DIR, true))
|
||||
}
|
||||
|
||||
/// Window ▸ `<ScriptUI panel>`: run the panel script (or bring its panel forward).
|
||||
fn open_panel(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = "window.scriptPanel";
|
||||
@@ -416,6 +510,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
always,
|
||||
restore_panels
|
||||
),
|
||||
cmd!(
|
||||
"file.runStartupScripts",
|
||||
"Run Startup Scripts",
|
||||
[],
|
||||
None,
|
||||
"{} → runs the scripts in Scripts/Startup in alphabetical order (frontends call it at launch) → {ran: [name], waiting: [name] (in a modal dialog), failed: [{name, error}]}",
|
||||
always,
|
||||
run_startup
|
||||
),
|
||||
cmd!(
|
||||
"file.runShutdownScripts",
|
||||
"Run Shutdown Scripts",
|
||||
[],
|
||||
None,
|
||||
"{} → closes the project, then runs the scripts in Scripts/Shutdown in alphabetical order (frontends call it as the app quits; their modal dialogs close as if canceled) → {ran: [name], waiting: [name], failed: [{name, error}]}",
|
||||
always,
|
||||
run_shutdown
|
||||
),
|
||||
crate::query!("script.settings.get", "Get Script Setting", "{section, key} → {have, value} (app.settings.haveSetting / getSetting)", settings_get),
|
||||
cmd!(
|
||||
"script.settings.save",
|
||||
|
||||
Reference in New Issue
Block a user