M6: form buttons show the side panels (execMenuItem ShowHide…)

This commit is contained in:
Eric Kaden
2026-10-19 20:59:14 -04:00
parent ec0a965074
commit e8eb840ec5
10 changed files with 147 additions and 8 deletions
+1
View File
@@ -657,6 +657,7 @@ fn request_json(r: &pdfcraft_engine::js::Request) -> Value {
R::Focus(f) => json!({ "focus": f }),
R::Beep => json!({ "beep": true }),
R::SaveAs => json!({ "save_as": true }),
R::ShowPanel(p) => json!({ "panel": format!("{p:?}").to_lowercase() }),
}
}
+9 -2
View File
@@ -7,7 +7,7 @@
//! off, scripts don't run and AF calls keep working natively.
use pdfcraft_forms::{Field, FieldChange, FieldEvent, FieldKind, FieldProps, ScriptResult, Scripts};
pub use pdfcraft_js::{Limits, Outcome, Request};
pub use pdfcraft_js::{Limits, Outcome, Panel, Request};
use crate::{DocId, Edit, EditError, Session};
@@ -29,7 +29,14 @@ impl JsOutput {
fn absorb(&mut self, o: &Outcome) {
self.alerts.extend(o.alerts.iter().cloned());
self.console.extend(o.console.iter().cloned());
self.requests.extend(o.requests.iter().cloned());
// A field's own scripts (keystroke, format, validate, calculate) run unasked: only a
// button's may show or hide a side panel.
for r in &o.requests {
match r {
Request::ShowPanel(_) => self.console.push("A field script asked to show a side panel; only a button may show a side panel".into()),
r => self.requests.push(r.clone()),
}
}
self.errors.extend(o.error.iter().cloned());
}
+27
View File
@@ -6106,3 +6106,30 @@ fn undo_never_makes_an_unsaved_document_clean() {
s.undo(new).unwrap();
assert!(!s.get(new).unwrap().dirty, "once saved, undo back to the saved state is clean");
}
#[test]
fn an_xfa_button_may_show_a_side_panel_and_other_events_may_not() {
let xdp = r##"<?xml version="1.0" encoding="UTF-8"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet><pageArea name="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
<field name="files" w="2in" h="0.3in"><ui><button/></ui><caption><value><text>Attachments</text></value></caption>
<event activity="click"><script contentType="application/x-javascript">app.execMenuItem("ShowHideFileAttachment");</script></event></field>
<field name="note" w="2in" h="0.3in"><ui><textEdit/></ui>
<event activity="exit"><script contentType="application/x-javascript">app.execMenuItem("ShowHideFileAttachment");</script></event></field>
</subform>
</template>
</xdp:xdp>
"##;
let mut s = Session::new();
let id = s.open("files.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(xdp)), None).expect("opens");
s.take_js_output(id);
s.apply(id, Edit::XfaEvent { som: "form[0].files[0]".into(), activity: "click".into() }).unwrap();
let out = s.take_js_output(id);
assert_eq!(out.requests, [pdfcraft_js::Request::ShowPanel(pdfcraft_js::Panel::Attachments)], "{out:?}");
s.apply(id, Edit::SetFieldValue { name: "note".into(), value: FieldValue::Text("x".into()) }).unwrap();
let out = s.take_js_output(id);
assert!(out.requests.is_empty(), "only a click shows a panel: {out:?}");
assert!(out.console.iter().any(|c| c.contains("show a side panel")), "{:?}", out.console);
}
+2
View File
@@ -53,6 +53,7 @@ fn click_only(e: &XfaEffect) -> Option<&'static str> {
XfaEffect::Print => Some("print"),
XfaEffect::SaveAs => Some("save"),
XfaEffect::LaunchUrl(_) => Some("open a link"),
XfaEffect::ShowPanel(_) => Some("show a side panel"),
XfaEffect::SetFocus(_) => Some("move the focus"),
_ => None,
}
@@ -626,6 +627,7 @@ impl<'a, 'b> Runner<'a, 'b> {
XfaEffect::Print => self.run.out.requests.push(Request::Print),
XfaEffect::SaveAs => self.run.out.requests.push(Request::SaveAs),
XfaEffect::LaunchUrl(u) => self.run.out.requests.push(Request::LaunchUrl(u.clone())),
XfaEffect::ShowPanel(p) => self.run.out.requests.push(Request::ShowPanel(*p)),
XfaEffect::SetFocus(som) => {
if let Some(name) = self.by_som().get(som).cloned() {
self.run.out.requests.push(Request::Focus(name));
+38 -1
View File
@@ -13,7 +13,7 @@
//! `required`, `display`, `hidden`, `numItems`, `getItemAt`, `currentValueIndices`,
//! `isBoxChecked`, `checkThisBox`, `exportValues`, `charLimit`, `textColor`, `fillColor`,
//! `setFocus`.
//! - `app` (`alert`, `beep`, `response`, `launchURL`, `viewerType`, `viewerVersion`,
//! - `app` (`alert`, `beep`, `response`, `launchURL`, `execMenuItem`, `viewerType`, `viewerVersion`,
//! `platform`, `setTimeOut`), `util` (`printf`, `printd`, `printx`), `console` (`println`,
//! `show`, `clear`), `display` and `color`.
//!
@@ -159,6 +159,19 @@ pub enum Request {
Beep,
/// `app.execMenuItem("SaveAs")` (XFA forms' Save buttons).
SaveAs,
/// `app.execMenuItem("ShowHide…")`: show the side panel, or hide it when it is showing.
ShowPanel(Panel),
}
/// A side panel a script can show or hide (`app.execMenuItem("ShowHideFileAttachment")`, …).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Panel {
Bookmarks,
/// Page thumbnails.
Pages,
Attachments,
Signatures,
Layers,
}
/// What running a script did.
@@ -565,6 +578,29 @@ fn launch_url(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsVa
Ok(JsValue::undefined())
}
/// What `app.execMenuItem(name)` asks for, for the menu items forms use: the side panels, Print
/// and Save As. Other items do nothing.
pub(crate) fn menu_item(name: &str) -> Option<Request> {
Some(match name {
"ShowHideFileAttachment" => Request::ShowPanel(Panel::Attachments),
"ShowHideBookmarks" => Request::ShowPanel(Panel::Bookmarks),
"ShowHideThumbnails" => Request::ShowPanel(Panel::Pages),
"ShowHideSignatures" => Request::ShowPanel(Panel::Signatures),
"ShowHideOptCont" => Request::ShowPanel(Panel::Layers),
"Print" => Request::Print,
"SaveAs" | "Save" => Request::SaveAs,
_ => return None,
})
}
fn exec_menu_item(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
let item = text(&arg(args, 0), ctx)?;
if let Some(r) = menu_item(&item) {
host(ctx)?.borrow_mut().requests.push(r);
}
Ok(JsValue::undefined())
}
fn response(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
// No one to answer: as if the dialog was cancelled.
Ok(JsValue::null())
@@ -931,6 +967,7 @@ fn install(ctx: &mut Context) -> JsResult<()> {
.function(NativeFunction::from_fn_ptr(beep), js_string!("beep"), 0)
.function(NativeFunction::from_fn_ptr(response), js_string!("response"), 1)
.function(NativeFunction::from_fn_ptr(launch_url), js_string!("launchURL"), 2)
.function(NativeFunction::from_fn_ptr(exec_menu_item), js_string!("execMenuItem"), 1)
.function(NativeFunction::from_fn_ptr(timer), js_string!("setTimeOut"), 2)
.function(NativeFunction::from_fn_ptr(timer), js_string!("setInterval"), 2)
.function(NativeFunction::from_fn_ptr(noop), js_string!("clearTimeOut"), 1)
+36
View File
@@ -1132,3 +1132,39 @@ mod session {
assert!(o.abandoned || o.error.as_deref().is_some_and(|e| e.contains("too long")), "{o:?}");
}
}
mod menu_items {
use super::*;
use crate::xfa::{XfaDoc, XfaEffect, XfaEvent, run_xfa};
#[test]
fn exec_menu_item_shows_side_panels_prints_and_saves() {
let o = go(
"app.execMenuItem('ShowHideFileAttachment'); app.execMenuItem('ShowHideBookmarks'); app.execMenuItem('ShowHideThumbnails'); \
app.execMenuItem('ShowHideSignatures'); app.execMenuItem('ShowHideOptCont'); app.execMenuItem('Print'); \
app.execMenuItem('SaveAs'); app.execMenuItem('NoSuchItem');",
&Event::field("Mouse Up", "total", ""),
);
assert_eq!(o.error, None);
assert_eq!(
o.requests,
[
Request::ShowPanel(Panel::Attachments),
Request::ShowPanel(Panel::Bookmarks),
Request::ShowPanel(Panel::Pages),
Request::ShowPanel(Panel::Signatures),
Request::ShowPanel(Panel::Layers),
Request::Print,
Request::SaveAs,
]
);
}
#[test]
fn xfa_scripts_show_the_attachments_panel() {
let event = XfaEvent { activity: "click".into(), target: "form1[0].page1[0].go[0]".into(), ..Default::default() };
let o = run_xfa("app.execMenuItem('ShowHideFileAttachment')", &event, &XfaDoc::default(), &super::xfa_model::form(), Limits::default());
assert_eq!(o.error, None, "{o:?}");
assert!(o.effects.contains(&XfaEffect::ShowPanel(Panel::Attachments)), "{:?}", o.effects);
}
}
+6 -3
View File
@@ -112,6 +112,8 @@ pub enum XfaEffect {
MessageBox(String),
/// `xfa.host.resetData()`: the named fields, or every field when empty.
ResetData(Vec<String>),
/// `app.execMenuItem("ShowHide…")`: show or hide a side panel.
ShowPanel(crate::Panel),
Print,
SaveAs,
LaunchUrl(String),
@@ -1049,9 +1051,10 @@ fn set_focus(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsVal
fn exec_menu_item(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
let item = text(&arg(args, 0), ctx)?;
match item.as_str() {
"SaveAs" | "Save" => effect(ctx, XfaEffect::SaveAs)?,
"Print" => effect(ctx, XfaEffect::Print)?,
match crate::menu_item(&item) {
Some(crate::Request::SaveAs) => effect(ctx, XfaEffect::SaveAs)?,
Some(crate::Request::Print) => effect(ctx, XfaEffect::Print)?,
Some(crate::Request::ShowPanel(p)) => effect(ctx, XfaEffect::ShowPanel(p))?,
_ => {}
}
Ok(JsValue::undefined())
+12 -1
View File
@@ -2,7 +2,7 @@
//! the JavaScript console (⌘J), Document JavaScripts, and Preferences ▸ JavaScript.
use egui::{Align, Layout};
use pdfcraft_engine::js::{JsOutput, Request};
use pdfcraft_engine::js::{JsOutput, Panel, Request};
use pdfcraft_engine::{DocId, Edit};
use crate::theme::{self, Tokens};
@@ -67,6 +67,17 @@ impl PdfCraftApp {
&[("u", &u)],
),
Request::SaveAs => self.run_command("file.save_as"),
Request::ShowPanel(p) => {
let panel = match p {
Panel::Bookmarks => crate::RightPanel::Bookmarks,
Panel::Pages => crate::RightPanel::Pages,
Panel::Attachments => crate::RightPanel::Attachments,
Panel::Signatures => crate::RightPanel::Signatures,
Panel::Layers => crate::RightPanel::Layers,
};
// Show/Hide: a panel already showing closes.
self.right = (self.right != Some(panel)).then_some(panel);
}
Request::Focus(_) | Request::Beep | Request::Reset(_) => {}
}
}
+15
View File
@@ -1072,3 +1072,18 @@ fn auto_size_fields_shrink_and_wrap_typed_text_to_fit_their_box() {
// An explicit size is kept, as in Acrobat: the text may then run past the box.
assert_eq!(on_screen_fit(&h, "fixed", long).0, 10.0);
}
#[test]
fn a_form_button_shows_and_hides_the_attachments_panel() {
let mut h = harness();
h.state_mut().set_option("panel", "none").unwrap();
h.run_steps(2);
let id = h.state().views[0].id;
h.state_mut().run_button_script(id, "size", "app.execMenuItem('ShowHideFileAttachment');");
h.run_steps(2);
assert_eq!(h.state().right, Some(pdfcraft_ui_egui::RightPanel::Attachments));
// Show/Hide, as its name says: asked again, it closes.
h.state_mut().run_button_script(id, "size", "app.execMenuItem('ShowHideFileAttachment');");
h.run_steps(2);
assert_eq!(h.state().right, None);
}
+1 -1
View File
@@ -5137,7 +5137,7 @@ status = "partial"
commands = ["tools.js_console"]
tools = ["js_run"]
evidence = ["crates/js/src/tests.rs::calculate_scripts_read_fields_and_set_the_value", "crates/engine/src/tests.rs::form_javascript_validates_calculates_and_formats", "crates/automation/tests/automation.rs::javascript_through_tools"]
notes = "boa (pure Rust) with the forms object model: event, getField/Field (value, readonly, display, colours, items, check boxes), app, util.printf/printd/printx, console, display, color; no annotations, layers or dialogs (app.response returns null) yet."
notes = "boa (pure Rust) with the forms object model: event, getField/Field (value, readonly, display, colours, items, check boxes), app (with execMenuItem for the side panels, Print and Save As, from a button only), util.printf/printd/printx, console, display, color; no annotations, layers or dialogs (app.response returns null) yet."
[[feature]]
id = "form.js-console"