How To Articles
by Thom Parker of WindJack Solutions.
Copyright
© 2004
by WindJack Solutions
Popup Menu Programming in
Acrobat® JavaScript
The popup menu is one the most useful and easy to use User
Interface Items available to the Acrobat JavaScript
programmer. Unlike many of the other UI options
(buttons, edit fields, lists, etc.) it can be used in Folder
level scripts as well as at the Document level and lower.
Document level scripting is primarily used for forms related
operations and document navigation, and for this use Acrobat
provides a rich set of UI elements that have been well covered
in the industry literature. Folder level scripting on
the other hand, is primarily used to either automate tasks in
Acrobat or to provide general utility functions for other
scripts. There are relatively few UI elements available
to the JS programmer at the folder level, making the popup
menu even more valuable for this purpose.
If you develop folder level scripts using popup menus there are
a couple of things you need to be aware of. First, a popup
menu will only appear on top of a document window, no open
documents means no menu. Second, Acrobat tries to place
the menu at the last cursor location, or as close as it can get
to it. You will have to watch carefully to see where the
menu "pops up." It may actually appear somewhere around
the edge of the document window. Debugging folder level scripts
is also a bit troublesome since Acrobat has to be restarted
every time the script is changed. See the article "Some
Notes on Developing Folder Level JavaScript".
Acrobat JavaScript provides 2 methods for creating
popup menus, app.popUpMenu (introduced in Acrobat 5.0), and
app.popUpMenuEx (introduced in Acrobat 6.0). Both of these
methods are very similar in how they operate. The newer
method, popUpMenuEx, is slightly more complex, but offers a
variety of useful options.
Basic Operation:
Let's start off looking at the
app.popUpMenu method. It's simpler and structurally both
methods are identical, so everything we do with the easier to use
method will apply to the other.
This method will take any number of
arguments. Each argument is either a string or an array of
strings. If we go with the simple case, strings
only, we can create a single level list style menu. The menu
items are the same as, and appear in the same order as the
arguments to the popUpMenu function.
var result = app.popUpMenu("apple",
"orange", "coconut", "mango");
The popup menu methods are blocking
functions. This means execution of the JavaScript stops at
this line until the user either selects an item from the menu or
cancels the selection. The value of the selected item is
returned and placed in the result variable. If the
user were to select apple from this
menu, the value return to the variable result would then
be apple.
If the selection is canceled, null is returned.
To create a sub menu we'll replace
one of the string arguments with
an array of strings. The first element of
the array is always the parent item and the following elements form the
items of the submenu.
app.popUpMenu("apple", ["oranges",
"navel", "valencia", "blood"], "coconut", "mango");
It is important to note that parent
items cannot be selected from the menu and therefore cannot be
returned by the popUpMenu method. In the example above, oranges is the only parent item in the menu and so it will
never be returned. The sub menu items immediately below
the parent item are called the child items, some of which
may also be parent items as we'll see in the next example.
Only menu items having no children can be returned from the popUpMenu method.
These items are called leaves. This
terminology is consistent with any hierarchical, or tree type
structure.
Simple so far, but what if we
want yet another level in the menu. Well, this is easy too.
It's just an extension of what we have already done, i.e.
replace any string, except the first element in an array, with
an array of strings. Let's reorganize the menu from the
previous example to try
this out.
app.popUpMenu("apple",["citrus", ["oranges","navel","valencia"],["gratefruit","pink","yellow"]]);
One last note on this topic.
The first element of any array is always the parent element of a
sub menu. If it is replaced with another array,
or any other data type, the string representation of this value
would still be the
parent item. The results of such a replacement may be a
funny looking and meaningless menu since none of the elements
of the replacing item will contribute to the menu
list.
app.popUpMenuEx:
Everything stated so far is also true
for the newer popUpMenuEx method. Just replace the word string
in the previous discussion with the word object, and
that's practically, but not entirely, all you need to know.
The new method was created to solve several problems with the
first version. The biggest problem being that the return value is the
name of the selected menu item. The construction of even
moderately complex menus is restricted by this method of operation. For
example, a menu constructed to control the hidden property of
a form field might look like this.
If this menu was constructed with the
popUpMenu method the return value is either hide
or show. This value tells you the action being
requested but not the name of the field to apply it to. To
resolve this issue you'd need to include all the information in
the name of the menu item. The menu item names can quickly
become too long to be useful as the menu becomes more complex.
The object arguments that
replace the use of string arguments for the app.popUpMenuEx method (referred to in the
Acrobat JavaScript Reference as the "MenuItem
Generic Object") has a set of properties that include both a
name for the menu item (cName) and a return value (cReturn). Having this separate
return value means you can give your menu items short and user
friendly names. The return value is optional and is
only used on "leaf" items. The name of the menu item is returned
if no specific return value is given.
Other entries in the menu item object
are the bMarked and bEnabled properties. These properties
control the state of a menu item and convey more meaningful
information to the user than the item name alone. They also
allow you to create more compact menus. For example, to
give the menu shown above more meaning, the bMarked
property can be used to place a check mark next to either show or
hide to indicate the current state of the field.
Alternatively, to make the menu more compact, a check mark can
be placed on a field item to indicate the visible state, thus
removing the need for a submenu.
A big difference in
how the two Popup Menu methods are used is in how the child items
are specified. The menu item object has a property, oSubMenu,
for specifying the child menu item objects, rather than
simply tacking them onto an array as we did previously.
The value of this property can be either a single menu item
object or an array of them. Below is the code for the fields menu shown above using the popUpMenuEx method
var strResult =
app.popUpMenuEx({cName:"field1",oSubMenu:[{cName:"show",cReturn:"field1:show"},
{cName:"hide", cReturn:"field1:hide"}]
},
{cName:"field2",oSubMenu:[{cName:"show",cReturn:"field2:show"},
{cName:"hide", cReturn:"field2:hide"}]
});
The result from this menu is a bit
more complicated than in the previous examples. If the user
selects the menu item field1->hide the returned value is
field1:hide. This value now needs to be parsed into two
separate values, 1) the action to perform and 2) the name of
the field
the action is applied to. The code line below shows a simple
method for accomplishing this parsing.
var arrayVals = strResult.split(":");
The result of this line is an array
of strings split out of the variable strResult that was
returned from the popUpMenuEx method, arrayVals[0]
contain the field name and arrayVals[1] contain the action to
be applied to the field.
Techniques for Using a Popup Menu:
In the examples shown so far all the
menu items are static. That is, they are explicitly set up as
constant strings, objects, and arrays in the code. This
technique is fine for all those menus for which you already have
defined entries ahead of time. But for those menus whose
contents are not known at the time you write the code, the
arguments to the popup menu method have to be built at run time.
The first problem we run into
becomes apparent when we try to programmatically build the
argument list in the first example. Let's say the list of fruits
is stored in an array, but we don't know the size of the array
until runtime. The app.popUpMenu method shown in the example
has 4 arguments, but if we don't know this up front we can't
hard code the method call with 4 arguments. Maybe the array will
contain 3 or 5 items, we just don't know. One way to
solve this problem is to rearrange the structure of the menu.
We'll pass just one argument. As shown in the following code
that one argument is an Array.
var arrayOfFruits = new Array("apple",
"orange", "coconut", "mango");
// assume the exact nature of the previous line is
unknown to our menu code.
......
// Start of menu code
var arrayParentItem = new Array("Fruits"); // Create
menu items array, "Fruits" is the parent item
var arrayMenuItems = arrayParentItem.concat(arrayOfFruits);
// Concatonate the Parent and the
// child item arrays
var strResult = app.popUpMenu(arrayMenuItems);
if(strResult != null)
app.alert("you have picked the
fruit: " + strResult);
This style of menu can be
quite useful when a menu header is necessary to provide some
extra information to the user. A good use would be for a
situation in which the user cannot be expected to know the exact
purpose of the menu from it's context or entries alone. In
this example the header shows it's a Fruits menu.
This change in menu
structure doesn't really solve the problem we started with. i.e.
how to pass an arbitrary number of arguments to the popUpMenu
method to make a flat list(1 level) menu. The ideal solution we are
looking for is some kind of argument list variable type.
In fact JavaScript Ver. 1.2 defines such an object. In
JavaScript, Functions are objects with properties and methods
just like other objects. One of these properties,
arguments, is an array of the values passed into the
function. Unfortunately, this property is only accessible
inside the function and cannot be passed into another function
as "arguments." However, there is a method all
functions have that we can use, the apply method.
The apply method
takes two arguments. The first is an object that will
become the this object inside the function. The
second argument is an array of values that will be used
as the arguments to the function. So now we can call the popUpMenu function with an arbitrary number of arguments.
app.popUpMenu.apply(
app, arrayOfFruits );
Notice the first
argument is app. The this object inside a function
represents the context the in which the function is called.
The context of popUpMenu function is app since it is a
member of app. The second argument is the array of
strings that are our top level menu items. The result
of this operation is a menu identical to the one in the
first example. Creating the menu this way is even easier
than in the previous technique where we used the menu header
item.
A Useful Example:
Now let's put everything
we've learned so far into creating a real world application. The
easiest things to express in a menu are those things
that are naturally a list or tree. Several items
in a PDF fit this format: Fields, Bookmarks, Annotations,
Links, Pages, Named Icons, Optional Content Groups, Templates,
Security Handlers, Acrobat Toolbar Buttons and Menu Items. Literally any property or method that returns an array,
or tree,
of items can be easily expressed as a menu. An Object's
properties and methods can also be displayed in a menu since an
object can be treated as an Array.
For this example we'll create a menu for manipulating the properties of a
form field
on the current document. We'll use the popUpMenuEx method
and both the bMarked and bEnabled properties to mark the menu item if the particular property is set
and disable a menu item if the property is not valid for
the field.
All of the work of creating a menu is
in building up a list of items. Typically this is
accomplished with a code loop for copying data from one list
(the source Array) to another (the menu item Array).
Submenus are created by nesting loops inside one another, one
nested loop for each menu level. In this example there are two
nested loops, so the resulting menu has two levels. The
outer loop assembles the top level menu of Field names and
the nested inner loop builds a submenu of properties for each field.
//** First we need a list
of names for the properties we are going to look at
var aryPropNames = ["hidden", "print", "readonly",
"required"];
//** Variables needed during the building of the menu item
list
// One for each step in the process
var aryMnuItms = new Array(); //** Array of top level
Menu Item Objects
var objFldItem; //** Menu Item Object for the
Field Names
var objField; //** Field
Object
var strFldName; //** Field Name.
var aryProps; //** Field SubMenu,
Array of menu item objects for each property
var objProp; //** Menu Item
Object for field properties
//** Outer Loop for building the Field item list
for(var i=0; i<this.numFields; i++)
{ //** Get the field object
strFldName =
this.getNthFieldName(i);
objField =
this.getField(strFldName);
//** Create menu item
object for this field
objFldItem = new
Object();
objFldItem.cName =
objField.type + ": " + strFldName; //** field type (button,
text, etc)
// and the field's name
//** Create the field
property sub menu Array
aryProps = new Array();
//** Inner Loop for
building the
field property sub menu
for(var j=0; j<aryPropNames.length;
j++)
{ //** Create menu item
object for property
objProp = new Object();
objProp.cName = aryPropNames[j]; //** menu item has same name as
property
try // ** Property may not be accessible so catch error
{ //** Menu Item marked if property is set to "true"
objProp.bMarked = objField[ aryPropNames[j]
];
}catch(e)
{ //** If property is not available, display a disabled
item
objProp.bEnabled = false;
}
//** Return value includes both the property and field
name
objProp.cReturn = strFldName + ":" + aryPropNames[j];
// Add property menu item object to the submenu array
aryProps.push( objProp );
}
//** Add submenu array to
the field menu item object
objFldItem.oSubMenu =
aryProps;
//** Add field menu item
to the array of top level menu item objects
aryMnuItms.push(
objFldItem );
}
//** Now the list of menu items is complete. From here
we can either create a single
// top level item to pass to the popUpMenuEx
method.
// ex: app.popUpMenuEx({cName:"Field
Properties",
oSubMenu: aryMnuItms});
// Or we can make the fields all top level items
// Execute the popup menu
var strResult = app.popUpMenuEx.apply(app, aryMnuItms);
if(strResult != null)
{
var aryResults = strResult.split(":");
// separate out field and property strings
objField = this.getField(
aryResults[0] ); // Get the selected field
var bVal = objField[
aryResults[1] ]; // Get the selected property value
// Toggle value of the
fields property
objField[ aryResults[1] ]
= !bVal;
}
The required and readonly
properties of a Field Object apply to all instances of the Field
on a document, but the hidden and print properties
apply only to the specific Widget instances of a Field. So
our script will work inconsistently for Fields with more than
one instance.
We can improve its' operation by adding another level of
submenus that reference the widget instances of the field.
The instance information is stored in the page property of the
Field. This property returns an Array if the field has more than
one instance. To modify the existing code it is only necessary
to add another nested loop using the page Array. As shown
in the "Full Code
for the example", this loop is inserted
in-between the two existing loops. As a matter of
convenience and good coding practices, the inner field
properties loop is bundled into a function. As you begin
to build larger and more complex menus, you should always place
cohesive sections of code like this into separate functions.
Full code for the example. The
JavaScript is executed from a toolbar button.
Below is a menu created with the
modified JavaScript.
A Recursive Example:
As noted previously,
many of the PDF JavaScript
objects have a tree structure. For some of these objects
the depth of the tree branches is indeterminate, it could
be anything. Both Acrobat's menus and a documents bookmarks fit
this description. Remember that we need to create a nested
loop for each level of submenus we want in the final menu.
In this situation we don't know how many levels we'll need so we
can't write the code with a fixed number of nested loops as in
the previous examples.
One solution to this problem is to use a recursively called
function.
A recursive function is one which calls itself until some stop
condition is reached.
For creating a menu from a
tree, this function is written as a single loop that builds one
layer of the menu hierarchy. For each tree node that has
children the function calls itself, effectively creating a
nested loop. The leaf nodes provide the stop condition. Since
a leaf doesn't have any children, the function returns rather than
calling itself.
In the following code we
build a menu of the Acrobat Menu Items. This may seem a
little redundant, but its' an excellent example and it has it's
uses. In this example the function
BuildMenuMenu
is passed an array of menu
items from which it builds an array of Menu Item Objects.
As it's doing this it calls itself for each array of child menus
it encounters in the current item it's processing. In this
particular case the object passed into
BuildMenuMenu
may be a single Menu Item rather than an array. To deal
with this situation and improve the quality and readability of
the code the recursive call is broken into two functions.
However, it could have easily been written as one function.
// Function to Process individual menuitems
function ProcessMenuItem(oAcroMenuItem)
{ // Explicitly create a new Menu Item Object and set the item
display name
var oRtnMenuItem = new Object();
oRtnMenuItem.cName = oAcroMenuItem.cName;
// if this item has children then do the recursive call
if(oAcroMenuItem.oChildren != null &&
oAcroMenuItem.oChildren.length>0)
oRtnMenuItem.oSubMenu = BuildMenuMenu(oAcroMenuItem.oChildren);
else // Leaf Node so set the menu item's return value
oRtnMenuItem.cReturn =
oAcroMenuItem.cName;
return oRtnMenuItem;
}
// Recursive Function for creating menu
item array
function
BuildMenuMenu(origMenuArray)
{// First initialize the variables we'll use
var newMenuArray = null; // Array Menu Item Ojects returned
var newMenuItem = null; // Menu Item Object Element for the
array
// Don't do anything if a null value was passed in
if(origMenuArray != null)
{// Explicitly create the output array
newMenuArray = new Array();
// Test to make sure that we do in
fact have an array as input
if(origMenuArray.length != null)
{ // Walk the array and process
each element
for(var i=0;i<origMenuArray.length;i++)
{
newMenuItem = ProcessMenuItem(origMenuArray[i]);
// Add the newly created Menu Item Object to the array
newMenuArray.push(newMenuItem);
}
}
else
{// Assume that we were passed a
single Menu Item instead of an Array
newMenuItem
= ProcessMenuItem(origMenuArray[i]);
// Add the newly created Menu Item Object to the array
newMenuArray.push(newMenuItem);
}
}
return newMenuArray;
}
// Pass the top level array of Acrobat Menu Items into the
recursive function
var arryItems = BuildMenuMenu(app.listMenuItems());
var cRslt = app.popUpMenuEx.apply(app,arryItems);
if(cRslt != null)
console.println("Selected Menu Item:" + cRslt);
With only small variations
this code can be used to walk any regular tree. A regular
tree is one where all nodes have the same properties.
A Non-Standard Example:
Not all situations are
easily put into a menu format, but with a little ingenuity there
is usually a way. This next example shows just such a case.
This script displays the current size of a document page in
inches and
allows the user to resize one of its edges.
//** Aquire the crop
box for the current page
var pageRect =
this.getPageBox({nPage:this.pageNum});
//** Calculate the crop box size in inches
var nWidth = (pageRect[2] - pageRect[0])/72;
var nHeight = (pageRect[1] - pageRect[3])/72;
//** Display a simple menu showing the crop box
dimensions.
// each dimension item has a submenu
for indicating the edge to change
var strSide = app.popUpMenu(["Height: " + nHeight
+ '\"',"Top","Bottom"]
,["Width: " + nWidth + '\"', "Left","Right"]);
if(strSide != null)
{ // Use response dialog to get the amount to add
or remove from the specified edge
var strMsg = "Enter
inches to add or remove from the " + strSide + " of the
Document\n";
strMsg += "Use a
negative number to reduce the size\n";
strMsg += "EX: -1.5
to remove 1 1/2\" from the " + strSide + " Side";
var strValue =
app.response({cQuestion: strMsg, cTitle: "Resize Page: " +
this.pageNum
, cLabel: "inches"});
if(strValue !=
null)
{ //**
Convert the returned value to points
var nPointVal = strValue * 72;
//** Apply point value to the specified crop box edge.
switch(strSide)
{
case "Top":
pageRect[1] = pageRect[1] + nInchVal;
break;
case "Bottom":
pageRect[3] = pageRect[3] - nInchVal;
break;
case "Left":
pageRect[0] = pageRect[0] - nInchVal;
break;
case "Right":
pageRect[2] = pageRect[2] + nInchVal;
break;
}
//** Set new page crop box
this.setPageBoxes({cBox: "Crop",nStart: this.pageNum, rBox:
pageRect});
}
}
This menu is useful just
for the convenient way it displays the page dimensions.
Much could be done to make this example more powerful.
For example, adding another layer to include the sizes of all
the page boxes. Below is a link to the full code, excuted
from a toolbar button.
Full Script with
Toolbar Button.
We hope this material was helpful to you.
If you have any questions or comments for us please send email to
info@windjack.com.
|