WindJack Logo

About Us


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.

 

[ << BACK TO HOW TO ]



Home | About Us | Contact Us

Site design by Terraform Creative