mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
Merge PR #3580: M6: form buttons show the side panels (app.execMenuItem)
crates/js/src/lib.rs: module doc app/util list has execMenuItem and scand; install() doc comment from main. crates/js/src/tests.rs, crates/engine/src/tests.rs: both sides' appended tests kept. parity/acrobat-features.toml: js notes: execMenuItem wording plus scand wording.
This commit is contained in:
+68
-1
@@ -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`, `scand`), `console` (`println`,
|
||||
//! `show`, `clear`), `display` and `color`.
|
||||
//!
|
||||
@@ -164,6 +164,36 @@ 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 {
|
||||
/// The document's bookmarks (outline).
|
||||
Bookmarks,
|
||||
/// Page thumbnails.
|
||||
Pages,
|
||||
/// The files attached to the document.
|
||||
Attachments,
|
||||
/// The document's signatures and their validity.
|
||||
Signatures,
|
||||
/// The document's layers (optional content).
|
||||
Layers,
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
/// The panel's name in automation output, fixed whatever the variant is called.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Panel::Bookmarks => "bookmarks",
|
||||
Panel::Pages => "pages",
|
||||
Panel::Attachments => "attachments",
|
||||
Panel::Signatures => "signatures",
|
||||
Panel::Layers => "layers",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What running a script did.
|
||||
@@ -629,6 +659,8 @@ fn message(v: &JsValue, key: &str, ctx: &mut Context) -> JsResult<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `app.alert(cMsg)`: records the message, given as a string or as an object with `cMsg`, and
|
||||
/// returns 1 (the OK button).
|
||||
fn alert(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let m = message(&arg(args, 0), "cMsg", ctx)?;
|
||||
host(ctx)?.borrow_mut().alert(m);
|
||||
@@ -636,17 +668,49 @@ fn alert(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue>
|
||||
Ok(JsValue::from(1))
|
||||
}
|
||||
|
||||
/// `app.beep()`: queues a request to beep.
|
||||
fn beep(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
host(ctx)?.borrow_mut().request(Request::Beep);
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `app.launchURL(cURL)`: queues a request to open the URL, given as a string or as an object
|
||||
/// with `cURL`.
|
||||
fn launch_url(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let u = message(&arg(args, 0), "cURL", ctx)?;
|
||||
host(ctx)?.borrow_mut().request(Request::LaunchUrl(u));
|
||||
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,
|
||||
// Save opens Save As too, as it always has for XFA forms' buttons: a click on a form
|
||||
// never overwrites the file unasked.
|
||||
"SaveAs" | "Save" => Request::SaveAs,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// `app.execMenuItem(name)`: queues the request [`menu_item`] gives for the name (Save opens
|
||||
/// Save As too); an unknown name does nothing.
|
||||
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())
|
||||
}
|
||||
|
||||
/// `app.response(…)`: always `null`, the answer for a canceled dialog, since no one is there to
|
||||
/// type one.
|
||||
fn response(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
|
||||
// No one to answer: as if the dialog was cancelled.
|
||||
Ok(JsValue::null())
|
||||
@@ -1271,6 +1335,8 @@ fn printx(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue>
|
||||
Ok(s(&out))
|
||||
}
|
||||
|
||||
/// The `color` object: each named color (`transparent`, `black`, `red`, `cyan`, `gray`, …) as
|
||||
/// an array of its color space (`T`, `G`, `RGB` or `CMYK`) followed by its components.
|
||||
fn color_object(ctx: &mut Context) -> JsObject {
|
||||
let mut entries: Vec<(&str, Vec<JsValue>)> = vec![
|
||||
("transparent", vec![s("T")]),
|
||||
@@ -1348,6 +1414,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)
|
||||
|
||||
@@ -1393,6 +1393,8 @@ mod session {
|
||||
use crate::xfa::*;
|
||||
use crate::xfa_session::{Script, XfaSession};
|
||||
|
||||
/// A JavaScript or FormCalc script for `activity` on `target`, against the test form with
|
||||
/// default limits and a 3 s timeout.
|
||||
fn script(source: &str, formcalc: bool, target: &str, activity: &str) -> Script {
|
||||
Script {
|
||||
source: source.into(),
|
||||
@@ -1404,6 +1406,9 @@ mod session {
|
||||
}
|
||||
}
|
||||
|
||||
/// JavaScript and FormCalc scripts run in turn in one session, each on the form the caller
|
||||
/// sends: nothing an earlier script changed or defined leaks into the next, a failing script
|
||||
/// doesn't end the session, and hundreds of scripts run on its one thread.
|
||||
#[test]
|
||||
fn scripts_of_one_event_run_on_one_thread_each_on_the_form_the_caller_sends() {
|
||||
let mut s = XfaSession::new(XfaDoc { page_count: 3, ..Default::default() });
|
||||
@@ -1443,6 +1448,8 @@ mod session {
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(10), "{:?}", started.elapsed());
|
||||
}
|
||||
|
||||
/// A JavaScript or FormCalc script whose engine takes longer to be ready than the script's
|
||||
/// time limit still runs and returns its result, and the session stays alive.
|
||||
#[test]
|
||||
fn a_scripts_time_starts_when_its_context_is_ready() {
|
||||
// The engine made slow to be ready (300 ms) under a 50 ms script limit: the script is
|
||||
@@ -1463,6 +1470,9 @@ mod session {
|
||||
assert_eq!(s.dead(), None);
|
||||
}
|
||||
|
||||
/// An endless JavaScript loop past its 200 ms limit is abandoned within 2 s and leaves the
|
||||
/// session dead, so nothing more runs in it; an endless FormCalc loop is abandoned or
|
||||
/// reported as running too long.
|
||||
#[test]
|
||||
fn a_script_past_its_time_is_abandoned_and_the_session_is_dead_after_it() {
|
||||
let mut s = XfaSession::new(XfaDoc::default());
|
||||
@@ -1717,6 +1727,52 @@ mod scand {
|
||||
}
|
||||
}
|
||||
|
||||
mod menu_items {
|
||||
use super::*;
|
||||
use crate::xfa::{XfaDoc, XfaEffect, XfaEvent, run_xfa};
|
||||
|
||||
/// Every side panel has a fixed id in automation output.
|
||||
#[test]
|
||||
fn each_panel_has_a_fixed_id_for_automation() {
|
||||
let ids = [Panel::Bookmarks, Panel::Pages, Panel::Attachments, Panel::Signatures, Panel::Layers].map(Panel::id);
|
||||
assert_eq!(ids, ["bookmarks", "pages", "attachments", "signatures", "layers"]);
|
||||
}
|
||||
|
||||
/// Each side panel item, Print and SaveAs make their request, in order; an unknown item
|
||||
/// makes none and is not an error.
|
||||
#[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,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// `app.execMenuItem` works in XFA scripts too: a click shows the attachments panel.
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
/// The check XFA forms run on opening: without both globals, or with a version below 2.8, it
|
||||
/// alerts that the viewer can't show the form.
|
||||
const XFA_CHECK: &str = "if (typeof(xfa_installed) == 'undefined' || typeof(xfa_version) == 'undefined' || xfa_version < 2.8) app.alert('old viewer'); \
|
||||
|
||||
+20
-4
@@ -115,6 +115,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),
|
||||
@@ -219,7 +221,7 @@ fn payload(e: &XfaEffect) -> usize {
|
||||
XfaEffect::AddInstance { som } | XfaEffect::RemoveInstance { som } => som.len(),
|
||||
XfaEffect::MessageBox(s) | XfaEffect::LaunchUrl(s) | XfaEffect::SetFocus(s) => s.len(),
|
||||
XfaEffect::ResetData(names) => names.iter().fold(0usize, |n, s| n.saturating_add(s.len()).saturating_add(std::mem::size_of::<String>())),
|
||||
XfaEffect::Print | XfaEffect::SaveAs | XfaEffect::Beep | XfaEffect::Recalculate | XfaEffect::Relayout => 0,
|
||||
XfaEffect::Print | XfaEffect::SaveAs | XfaEffect::Beep | XfaEffect::Recalculate | XfaEffect::Relayout | XfaEffect::ShowPanel(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1131,39 +1133,50 @@ fn resolver(ctx: &mut Context, from: usize, many: bool) -> JsResult<JsValue> {
|
||||
|
||||
// ── xfa.host, xfa.layout, xfa.event, app, console ───────────────────────────────────────────
|
||||
|
||||
/// Records effect `e` on the host, as [`XHost::push`] does.
|
||||
fn effect(ctx: &mut Context, e: XfaEffect) -> JsResult<()> {
|
||||
host(ctx)?.borrow_mut().push(e);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `xfa.host.messageBox(message)`: records the message, cut to [`MAX_LINE`] characters, and
|
||||
/// returns 1.
|
||||
fn message_box(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let m = clip(text(&arg(args, 0), ctx)?);
|
||||
effect(ctx, XfaEffect::MessageBox(m))?;
|
||||
Ok(JsValue::from(1.0))
|
||||
}
|
||||
|
||||
/// `xfa.host.resetData(names)`: asks to reset the comma-separated fields, or every field when
|
||||
/// the list is empty.
|
||||
fn reset_data(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let list = text(&arg(args, 0), ctx)?;
|
||||
effect(ctx, XfaEffect::ResetData(reset_names(&list)))?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `xfa.host.print(…)`: asks to print; its arguments are ignored.
|
||||
fn print(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
effect(ctx, XfaEffect::Print)?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `xfa.host.beep()`: asks to beep.
|
||||
fn beep(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
effect(ctx, XfaEffect::Beep)?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `xfa.host.gotoURL(url)`: asks to open the URL.
|
||||
fn goto_url(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let u = text(&arg(args, 0), ctx)?;
|
||||
effect(ctx, XfaEffect::LaunchUrl(u))?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `xfa.host.setFocus(target)`: asks to focus the node, given as a node object or as a SOM
|
||||
/// expression resolved from the current node; an expression that names no node is passed on
|
||||
/// as written.
|
||||
fn set_focus(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let target = arg(args, 0);
|
||||
let som = match target.as_object() {
|
||||
@@ -1182,11 +1195,14 @@ fn set_focus(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsVal
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `app.execMenuItem(name)`: the effect for the request the shared menu table gives the name
|
||||
/// (Save opens Save As too); an unknown name does nothing.
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user