mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 10:20:47 -06:00
* #1335: accessibility slice 2: Info, Flowchart, Script Console and the 3D Camera Tracker block named, described and keyboard-operable The audit gets read-outs, graphics and logs as described items (a name is required in a fixed panel), dedicated maximized scenarios for the panels the shared loop squeezes, and minimum counts for the new zero panels. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Review: Enter/Space on a favourite star only toggles it; keyboard activation of a property label; settings page scrolls to focus; sharper UI test assertions Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Refresh the accessibility audit baseline after merging upstream/main Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * a11y audit: hold the machine-dependent Media Browser scenario to its upper bound The Media Browser opens on the home folder, so a runner with fewer files finds fewer controls and the 'improved, lower the entry' check failed in CI. The entry stays an upper bound. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Merge upstream/main and updated feat-gap25-a11y-audit into feat-gap25-a11y-panels-2 Brought in the updated feat-gap25-a11y-audit (which already contains upstream/main). Conflict: crates/ui-egui/src/menus.rs, which upstream split into menus/*.rs. Took the slim module and re-applied this branch's Flowchart hunks where the code now lives: nudge() skips the Flowchart (menus/invoke.rs) and Enter is left to the Flowchart in handle_shortcuts (menus/shortcuts.rs). Also removed a duplicate MACHINE_DEPENDENT const in tests/ui_accessibility_audit.rs that the two branches both added. The audit BASELINE still matches the measured findings. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Flowchart: split show() to pass the stricter clippy limits from main Main now denies cognitive_complexity (30) and too_many_arguments; the merged flowchart panel exceeded it. The keyboard navigation, the key-to-node mapping, pan-into-view, node commands, the accessible node name and the option params move into small helpers with unchanged behaviour. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Camera tracker a11y test: wait for the analysis result instead of one frame The test pressed Enter, ran one frame and expected to see the background job. On a fast runner the job starts, ends and is written back inside that frame, so the state was never seen. Undo the first analysis, press Enter, and wait (bounded) for the new analysis step in the undo history. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Accessibility: one hours range for the accessibility pass in ui-parity.md; test that Flowchart automation labels stay English Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Flowchart toolbar: keep the English label for the automation registration The translated text is shown and spoken; the automation label stays English. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: OpenAI Codex <noreply@openai.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
160 lines
7.0 KiB
Rust
160 lines
7.0 KiB
Rust
//! Window ▸ Script Console: a JavaScript REPL over the After Effects-style scripting object model
|
|
//! (`app.project`, comps, layers, properties…). Each run goes through the `script.run` command
|
|
//! in the console's persistent context, so variables survive between runs; edits are undoable.
|
|
|
|
use crate::i18n::tr;
|
|
use egui::{Align2, Rect, Sense, pos2, vec2};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::theme::Tokens;
|
|
use crate::{EffectcraftApp, a11y, widgets};
|
|
|
|
/// One line of console output.
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct ConsoleLine {
|
|
pub text: String,
|
|
/// `input`, `output`, `result` or `error`.
|
|
pub kind: String,
|
|
}
|
|
|
|
/// Script Console state (serde, so agents can read it).
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct ScriptConsole {
|
|
pub input: String,
|
|
pub log: Vec<ConsoleLine>,
|
|
/// Previously run inputs (Up/Down recall).
|
|
#[serde(default)]
|
|
pub history: Vec<String>,
|
|
}
|
|
|
|
impl ScriptConsole {
|
|
fn push(&mut self, kind: &str, text: &str) {
|
|
for l in text.lines() {
|
|
self.log.push(ConsoleLine { text: l.to_string(), kind: kind.to_string() });
|
|
}
|
|
if self.log.len() > 2000 {
|
|
let extra = self.log.len() - 2000;
|
|
self.log.drain(..extra);
|
|
}
|
|
}
|
|
|
|
/// Record a `script.run` result.
|
|
pub fn record(&mut self, code: &str, r: &Result<Value, String>) {
|
|
self.push("input", &format!("> {}", code.trim()));
|
|
match r {
|
|
Ok(v) => {
|
|
if let Some(o) = v["output"].as_str().filter(|o| !o.is_empty()) {
|
|
self.push("output", o);
|
|
}
|
|
if let Some(e) = v.get("error").filter(|e| !e.is_null()) {
|
|
let line = e["line"].as_u64().map(|l| format!("line {l}: ")).unwrap_or_default();
|
|
self.push("error", &format!("{line}{}", e["message"].as_str().unwrap_or("error")));
|
|
} else if !v["result"].is_null() {
|
|
self.push("result", &v["result"].to_string());
|
|
}
|
|
}
|
|
Err(e) => self.push("error", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run the console input.
|
|
pub fn run(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
|
let code = app.ui.script_console.input.trim().to_string();
|
|
if code.is_empty() {
|
|
return;
|
|
}
|
|
let r = crate::menus::invoke(app, ctx, "script.run", json!({"code": code, "name": "Script Console", "console": true}));
|
|
let con = &mut app.ui.script_console;
|
|
con.record(&code, &r);
|
|
if con.history.last() != Some(&code) {
|
|
con.history.push(code);
|
|
}
|
|
con.input.clear();
|
|
}
|
|
|
|
/// The Script Console panel: the output log (newest lines at the bottom, coloured by kind), the
|
|
/// multi-line script input and Run/Clear. Run — the button or ⌘/Ctrl+Enter while typing — hands the
|
|
/// input to the script runner; Clear only empties the log. The input text and log are UI state.
|
|
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|
let t = app.tokens;
|
|
let input_h = 86.0;
|
|
let bar_h = 26.0;
|
|
let out_rect = Rect::from_min_max(rect.min, pos2(rect.max.x, rect.max.y - input_h - bar_h));
|
|
let p = ui.painter().with_clip_rect(rect);
|
|
p.rect_filled(out_rect, 0.0, t.field_bg);
|
|
// Output, newest at the bottom.
|
|
let line_h = 16.0;
|
|
let visible = ((out_rect.height() - 8.0) / line_h).max(1.0) as usize;
|
|
let log = &app.ui.script_console.log;
|
|
let start = log.len().saturating_sub(visible);
|
|
for (i, l) in log[start..].iter().enumerate() {
|
|
let color = match l.kind.as_str() {
|
|
"error" => t.danger,
|
|
"input" => t.text_faint,
|
|
"result" => t.accent,
|
|
_ => t.text,
|
|
};
|
|
p.text(pos2(out_rect.min.x + 8.0, out_rect.min.y + 4.0 + i as f32 * line_h), Align2::LEFT_TOP, &l.text, Tokens::mono(11.5), color);
|
|
}
|
|
if log.is_empty() {
|
|
p.text(
|
|
out_rect.center(),
|
|
Align2::CENTER_CENTER,
|
|
crate::i18n::tr("JavaScript with the After Effects scripting object model — try app.project.numItems"),
|
|
Tokens::ui(12.0),
|
|
t.text_faint,
|
|
);
|
|
}
|
|
app.auto.add("scriptConsole.output", out_rect, "Script Console output");
|
|
// The output is painted text; a screen reader gets the same lines (the newest 200) as a log.
|
|
let tail = app.ui.script_console.log.len().saturating_sub(200);
|
|
let text: Vec<&str> = app.ui.script_console.log.iter().skip(tail).map(|l| l.text.as_str()).collect();
|
|
a11y::log(ui, egui::Id::unique("script-console-output"), out_rect, tr("Script Console output"), &text.join("\n"));
|
|
// Input.
|
|
let in_rect = Rect::from_min_size(pos2(rect.min.x + 4.0, out_rect.max.y + 4.0), vec2(rect.width() - 8.0, input_h - 8.0));
|
|
let mut run_now = false;
|
|
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(in_rect));
|
|
let te = egui::TextEdit::multiline(&mut app.ui.script_console.input)
|
|
.id(egui::Id::unique("script-console-input"))
|
|
.font(Tokens::mono(12.0))
|
|
.desired_width(in_rect.width())
|
|
.desired_rows(4)
|
|
// Tab leaves the field (it would otherwise type a tab character and trap the keyboard).
|
|
.lock_focus(false)
|
|
.hint_text(tr("Script… (⌘/Ctrl+Enter runs)"));
|
|
let resp = child.add_sized(in_rect.size(), te);
|
|
a11y::name(&resp, tr("Script… (⌘/Ctrl+Enter runs)"));
|
|
if resp.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter) && i.modifiers.command) {
|
|
run_now = true;
|
|
}
|
|
app.auto.add("scriptConsole.input", in_rect, "Script Console input");
|
|
// Buttons.
|
|
let bar = Rect::from_min_size(pos2(rect.min.x, rect.max.y - bar_h), vec2(rect.width(), bar_h));
|
|
let run_r = Rect::from_min_size(pos2(bar.max.x - 64.0, bar.min.y + 2.0), vec2(56.0, 20.0));
|
|
let clear_r = Rect::from_min_size(pos2(bar.max.x - 128.0, bar.min.y + 2.0), vec2(56.0, 20.0));
|
|
let run_resp = ui.interact(run_r, egui::Id::unique("script-console-run"), Sense::click());
|
|
p.rect_filled(run_r, 3.0, if run_resp.hovered() { t.accent_hover } else { t.accent });
|
|
p.text(run_r.center(), Align2::CENTER_CENTER, tr("Run"), Tokens::ui(12.0), egui::Color32::WHITE);
|
|
let clear_resp = ui.interact(clear_r, egui::Id::unique("script-console-clear"), Sense::click());
|
|
p.rect_stroke(clear_r, 3.0, egui::Stroke::new(1.0, t.field_border), egui::StrokeKind::Inside);
|
|
p.text(clear_r.center(), Align2::CENTER_CENTER, tr("Clear"), Tokens::ui(12.0), t.text);
|
|
a11y::button(&run_resp, tr("Run"), !app.ui.script_console.input.trim().is_empty());
|
|
a11y::button(&clear_resp, tr("Clear"), !app.ui.script_console.log.is_empty());
|
|
widgets::focus_outline(ui, &run_resp, &t);
|
|
widgets::focus_outline(ui, &clear_resp, &t);
|
|
app.auto.add("scriptConsole.run", run_r, "Run");
|
|
app.auto.add("scriptConsole.clear", clear_r, "Clear");
|
|
if run_resp.clicked() {
|
|
run_now = true;
|
|
}
|
|
if clear_resp.clicked() {
|
|
app.ui.script_console.log.clear();
|
|
}
|
|
if run_now {
|
|
let ctx = ui.ctx().clone();
|
|
run(app, &ctx);
|
|
}
|
|
}
|