Button Box HOW-TO: Making a Graphics Generator

P.J. Hinton
Mathematica Programming Group
Wolfram Research, Inc.

Last Revised: April 25, 1998

Introduction

Mathematica 3.0 introduced the use of ButtonBoxes that allow users to create point-and-click interactive elements in notebooks.  Perhaps the most noticeable example of buttons is in palettes.  Palettes are simply Mathematica notebooks that contain a grid of buttons, each of which perform a pasting action.

The Mathematica Book describes the low level structures that are used to define and adjust buttons, but there still is a lack of step-by-step walkthroughs on how to create a button that does more than just pasting, evaluating, or hyperlinking.  This notebook attempts to help users to learn some of the important points to keep in mind when making custom buttons.  These techniques are suitable for situations where the front end's button creation dialogs are too awkward of an interface.

The Problem

Suppose we wanted to create a button that has generates a plot in a standalone notebook window.  This is a nontrivial task because there is no ButtonStyle in the common style sheet notebooks that defines this sort of action.   If we address some key questions before writing the code for the button, our work will be much easier.

Question 1 – What portion of Mathematica will be performing the action associated with depressing the button?

Buttons can induce actions in the kernel, the front end, or both.  Sometimes the distinctions are not so clear.  We know that the creation of a notebook window is a front end function, but the creation of a graphics object is a kernel function.

The kernel function NotebookPut (Mathematica book, Section 2.10.2) allows one to build a notebook expression within the kernel and then pass it on to the front end for display.  It makes more sense to define our button such that the action is performed in the kernel.

The mechanism for specifying where the button's action will take place is the ButtonBox option ButtonEvaluator (Reference Guide entry for ButtonBox).  We don't care which kernel is used for the evaluation, so we set ButtonEvaluator to be Automatic.  That means that the kernel used by default by the front end will be used in the button's evaluation.

Question 2 – What Mathematica expressions are to be used to perform the button's action?

The action perfomed by a button is specified by the ButtonFunction option.  We indirectly answered part of our question in the prior subsection.  NotebookPut will be the outermost function, but we must also provide a Notebook expression for delivery to the front end.

ButtonFunction :>NotebookPut[notebook expression]

The notebook will have a lone cell with a with a PostScript GraphicsData expression (Mathematica Book, Section 2.10.7).  To clean up the appearance of the cell some, we can set the Cell option ShowCellBracket to False (Mathematica Book, Section 2.10.9).

Cell[GraphicsData["PostScript", string of Mathematica abbreviated PostScript], ShowCellBracket -> False]

The evaluation of a plotting function returns a graphics object.  We can bridge the gap between graphics object and PostScript with the DisplayString function (Reference Guide entry for DisplayString)

DisplayString[graphics object]

We'll stop short of adding the actual plotting function here.  We'll specify that elsewhere.  Doing this leaves open the opportunity for us to reuse this code for displaying any plot.  To preserve this generality, we have to rewrite our button function as a pure function (Mathematica Book, Section 2.2.5)

ButtonFunction :> (NotebookPut[Notebook[{Cell[GraphicsData["PostScript", DisplayString[#2]],ShowCellBracket -> False]}]]& )

Note that we have to wrap the pure function in parentheses because the postfix operator for Function has a lower precedence than the infix operator for RuleDelayed (Mathematica Book, Section A.2.7).  The reason we use #2 in lieu of just # will be made clear later on.

Once we get things working, we can always go back and adjust notebook level functions to tailor the appearance of the window to our liking.

Question 3 – What arguments are to be supplied to the the button's function?

Mathematica supplies two ButtonBox options which allow you to specify up to two arguments to a ButtonFunction – ButtonSource and ButtonData.  At first glance, these option names seem to a bit obtuse, not spelling out very clearly what purpose that they might serve.  If my ButtonFunction takes two arguments, which argument is ButtonSource and which one is ButtonData?  What if my ButtonFunction takes only one argument?.

ButtonSource is intended to be an argument that can refer to a specific location or region in a notebook (Mathematica Book, Section 2.10.6).  ButtonData contains hard-coded information that you may want to keep hidden behind the scenes.

The command that we will use to generate the plot is highly specific.  Moreover, because the function syntax may be long, we probably don't want to place the plotting command anywhere where the user may see it.  It becomes clear that ButtonData is the option we will use to house the plotting command.

We have no use for a ButtonSource here.  Since the ButtonData value is considered to be the option that gives the second argument, we now see why we chose #2 in our arguments to the ButtonFunction value despite the fact that there is only one argument to the function.

In our example, we will choose a Plot3D function.

ButtonData :> Plot3D[Sin[x] Cos[y],{x,0,2Pi}, {y,0,2Pi},DisplayFunction -> Identity]

The DisplayFunction value of Identity keeps the plot from being generated in the notebook where the button sits as well (Mathematica Book, Section 2.9.14).

Question 4 – What should the button say, and how do I enter it?

The first part of this question is a generic user design decision that shouldn't be taken too lightly, lest your poor choices wind up in the User Interface Hall of Shame ☺.  In our case, we'll label the button with a description of exactly what the button does.

The label of the button is specified in the first argument to ButtonBox.  You can supply either a string or  boxes.  We'll have some troubles trying to squeeze in the inline cells above, but we can work around this by pasting the cell's contents into an Input cell.  This converts the TextData into BoxData.  Then we can unformat the cell with the front end Show Expression menu item and copy the composite row box.  Here is the result we get from our label.

RowBox[{"Generate", " ", "a", " ", "plot", " ", "of", " ", 
      FormBox[
        RowBox[{
          RowBox[{"sin", "(", "x", ")"}], " ", 
          RowBox[{"cos", "(", "y", ")"}]}],
        "TraditionalForm"], " ", "over", " ", 
      RowBox[{
        FormBox[
          RowBox[{
            RowBox[{"[", 
              RowBox[{"0", ",", "2π"}], "]"}], " ", "\[Cross]", 
            " ", 
            RowBox[{"[", 
              RowBox[{"0", ",", "2π"}], "]"}]}],
          "TraditionalForm"], "."}]}]

The Solution

By putting a lot of effort into forethought, the solution now becomes very straightforward.  We can cobble together a button box and display it in the notebook for testing by wrapping the ButtonBox in DisplayForm.  Go ahead and evaluate this Input cell and then set the resulting output cell's Active option to be True.  You can set the option using the Cell Properties menu item.  This will make button presses evaluate the ButtonFunction.

ButtonBox[RowBox[{"Generate", " ", "a", " ", "plot", " ", "of", " ", 
      FormBox[
        RowBox[{
          RowBox[{"sin", "(", "x", ")"}], " ", 
          RowBox[{"cos", "(", "y", ")"}]}],
        "TraditionalForm"], " ", "over", " ", 
      RowBox[{
        FormBox[
          RowBox[{
            RowBox[{"[", 
              RowBox[{"0", ",", "2π"}], "]"}], " ", "\[Cross]", 
            " ", 
            RowBox[{"[", 
              RowBox[{"0", ",", "2π"}], "]"}]}],
          "TraditionalForm"], "."}]}], ButtonEvaluator -> "Automatic",ButtonFunction :> (NotebookPut[Notebook[{Cell[GraphicsData["PostScript", DisplayString[#2]],ShowCellBracket -> False]}]]& ), ButtonData :> Plot3D[Sin[x] Cos[y],{x,0,2Pi}, {y,0,2Pi},DisplayFunction -> Identity]] // DisplayForm

Polishing up the Work

We can avoid having to set the Active option at the cell level every time we rebuild the button by setting this option at the ButtonBox level.  The appearance of the output window generated by this button can be improved by adjusting Notebook level options like WindowSize, WindowElements, and WindowTitle.  Wrapping a StyleBox around the button's contents allows us to adjust the used in displaying the button text.  Inserting some newline characters will help to reduce the width of the ButtonBox.  Evaluate this Input cell to see the improvements.

ButtonBox[StyleBox[RowBox[{"Generate", " ", "a", " ", "plot", " ", "of", "\n", 
      FormBox[
        RowBox[{
          RowBox[{"sin", "(", "x", ")"}], " ", 
          RowBox[{"cos", "(", "y", ")"}]}],
        "TraditionalForm"], "\n", "over", " ", 
      RowBox[{
        FormBox[
          RowBox[{
            RowBox[{"[", 
              RowBox[{"0", ",", "π"}], "]"}], " ", "\[Cross]", 
            " ", 
            RowBox[{"[", 
              RowBox[{"0", ",", "π"}], "]"}]}],
          "TraditionalForm"], "."}]}], FontFamily -> "Helvetica"], ButtonEvaluator -> "Automatic",ButtonFunction :> (NotebookPut[Notebook[{Cell[GraphicsData["PostScript", DisplayString[#2]],ShowCellBracket -> False]}, WindowSize -> {300,250},  WindowElements -> {},WindowTitle->"Graphics"]]& ), ButtonData :> Plot3D[Sin[x] Cos[y],{x,0,2Pi}, {y,0,2Pi},DisplayFunction -> Identity], Active -> True] // DisplayForm

Reusing your Work

Earlier in this notebook, we took some pains in separating task-specific material from things that might be reusable.  Gathering the set of reusable ButtonBox objects together, we can place them in a StyleData cell in a style sheet so that you can speed the creation of similar buttons.  Here is an example of what a cell style might look like.

Cell[StyleData["PutGraphicInWindow"],StyleMenuListing->None,ButtonStyleMenuListing->Automatic, ButtonBoxOptions->{ButtonEvaluator -> "Automatic",ButtonFunction :> (NotebookPut[Notebook[{Cell[GraphicsData["PostScript", DisplayString[#2]],ShowCellBracket -> False]}, WindowSize -> {300,250},  WindowElements -> {},WindowTitle->"Graphics"]]& ),Active -> True}] // CellPrint

Setting the Cell option StyleMenuListing  to None makes sure that the prototype does not appear on the Format ->Style menu, and setting ButtonStyleMenuListing to Automatic makes the ButtonStyle available from the Input -> Create Button menu.  If you do choose to use the Edit Button dialog box facility for editing a button like this, the input on the ButtonData blank must be of the form:

ToExpression[string containing plot command]

In our example, the input would look like:

ToExpression["Plot3D[Sin[x] Cos[y],{x,0,2Pi}, {y,0,2Pi}, \!\(\*
StyleBox[\(DisplayFunction\),
"InlineInput"]\)\!\(\*
StyleBox[\(\\ \),
"InlineInput"]\)\!\(\*
StyleBox[\(->\),
"InlineInput"]\)\!\(\*
StyleBox[\(\\ \),
"InlineInput"]\)\!\(\*
StyleBox[\(Identity\),
"InlineInput"]\)]"]