# A Dialog-Based Macro

Now we will look at a more complicated macro which will demonstrate some useful techniques and BeanShell features.

## Use of the Macro

Our new example adds prefix and suffix text to a series of selected lines. This macro can be used to reduce typing for a series of text items that must be preceded and following by identical text. In Java, for example, if we are interested in making a series of calls to `StringBuffer.append()` to construct a lengthy, formatted string, we could type the parameter for each call on successive lines as follows:

    profileString_1
    secretThing.toString()
    name
    address
    addressSupp
    city
    state/province
    country

Our macro would ask for input for the common prefix and suffix to be applied to each line; in this case, the prefix is `ourStringBuffer.append(` and the suffix is `);`. After selecting these lines and running the macro, the resulting text would look like this:

    ourStringBuffer.append(profileString_1);
    ourStringBuffer.append(secretThing.toString());
    ourStringBuffer.append(name);
    ourStringBuffer.append(address);
    ourStringBuffer.append(addressSupp);
    ourStringBuffer.append(city);
    ourStringBuffer.append(state/province);
    ourStringBuffer.append(country);

## Listing of the Macro

The macro script follows. You can find it in the jEdit distribution in the `Text` subdirectory of the `macros` directory. You can also try it out by invoking Macros>Text>Add Prefix and Suffix.

    // beginning of Add_Prefix_and_Suffix.bsh

    // import statement (see )
    import javax.swing.border.*;

    // main routine
    void prefixSuffixDialog()
    {
        // create dialog object (see )
        title = Add prefix and suffix to selected lines;
        dialog = new JDialog(view, title, false);
        content = new JPanel(new BorderLayout());
        content.setBorder(new EmptyBorder(12, 12, 12, 12));
        content.setPreferredSize(new Dimension(320, 160));
        dialog.setContentPane(content);

        // add the text fields (see )
        fieldPanel = new JPanel(new GridLayout(4, 1, 0, 6));
        prefixField = new HistoryTextField(macro.add-prefix);
        prefixLabel = new JLabel(Prefix to add:);
        suffixField = new HistoryTextField(macro.add-suffix);
        suffixLabel = new JLabel(Suffix to add:);
        fieldPanel.add(prefixLabel);
        fieldPanel.add(prefixField);
        fieldPanel.add(suffixLabel);
        fieldPanel.add(suffixField);
        content.add(fieldPanel, Center);

        // add a panel containing the buttons (see )
        buttonPanel = new JPanel();
        buttonPanel.setLayout(new BoxLayout(buttonPanel,
            BoxLayout.X_AXIS));
        buttonPanel.setBorder(new EmptyBorder(12, 50, 0, 50));
        buttonPanel.add(Box.createGlue());
        ok = new JButton(OK);
        cancel = new JButton(Cancel);
        ok.setPreferredSize(cancel.getPreferredSize());
        dialog.getRootPane().setDefaultButton(ok);
        buttonPanel.add(ok);
        buttonPanel.add(Box.createHorizontalStrut(6));
        buttonPanel.add(cancel);
        buttonPanel.add(Box.createGlue());
        content.add(buttonPanel, South);

        // register this method as an ActionListener for
        // the buttons and text fields (see )
        ok.addActionListener(this);
        cancel.addActionListener(this);
        prefixField.addActionListener(this);
        suffixField.addActionListener(this);

        // locate the dialog in the center of the
        // editing pane and make it visible (see )
        dialog.pack();
        dialog.setLocationRelativeTo(view);
        dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        dialog.setVisible(true);

        // this method will be called when a button is clicked
        // or when ENTER is pressed (see )
        void actionPerformed(e)
        {
            if(e.getSource() != cancel)
            {
                processText();
            }
            dialog.dispose();
        }

        // this is where the work gets done to insert
        // the prefix and suffix (see )
        void processText()
        {
            prefix = prefixField.getText();
            suffix = suffixField.getText();
            if(prefix.length() == 0 && suffix.length() == 0)
                return;
            prefixField.addCurrentToHistory();
            suffixField.addCurrentToHistory();

            // text manipulation begins here using calls
            // to jEdit methods  (see )
            buffer.beginCompoundEdit();
            selectedLines = textArea.getSelectedLines();
            for(i = 0; i < selectedLines.length; ++i)
            {
                offsetBOL = textArea.getLineStartOffset(
                    selectedLines[i]);
                textArea.setCaretPosition(offsetBOL);
                textArea.goToStartOfWhiteSpace(false);
                textArea.goToEndOfWhiteSpace(true);
                text = textArea.getSelectedText();
                if(text == null) text = "";
                textArea.setSelectedText(prefix + text + suffix);
            }
            buffer.endCompoundEdit();
        }
    }

    // this single line of code is the script's main routine
    // (see )
    prefixSuffixDialog();

    // end of Add_Prefix_and_Suffix.bsh

## Analysis of the Macro

### Import Statements

    // import statement
    import javax.swing.border.*;

This macro makes use of classes in the `javax.swing.border` package, which is not automatically imported. As we mentioned previously (see ), jEdit's implementation of BeanShell causes a number of classes to be automatically imported. Classes that are not automatically imported must be identified by a full qualified name or be the subject of an `import` statement.

### Create the Dialog

    // create dialog object
    title = Add prefix and suffix to selected lines;
    dialog = new JDialog(view, title, false);
    content = new JPanel(new BorderLayout());
    content.setBorder(new EmptyBorder(12, 12, 12, 12));
    dialog.setContentPane(content);

To get input for the macro, we need a dialog that provides for input of the prefix and suffix strings, an OK button to perform text insertion, and a Cancel button in case we change our mind. We have decided to make the dialog window non-modal. This will allow us to move around in the text buffer to find things we may need (including text to cut and paste) while the macro is running and the dialog is visible.

The Java object we need is a JDialog object from the Swing package. To construct one, we use the `new` keyword and call a constructor function. The constructor we use takes three parameters: the owner of the new dialog, the title to be displayed in the dialog frame, and a boolean parameter (`true` or `false`) that specifies whether the dialog will be modal or non-modal. We define the variable `title` using a string literal, then use it immediately in the JDialog constructor.

A JDialog object is a window containing a single object called a content pane. The content pane in turn contains the various visible components of the dialog. A JDialog creates an empty content pane for itself as during its construction. However, to control the dialog's appearance as much as possible, we will separately create our own content pane and attach it to the JDialog. We do this by creating a JPanel object. A JPanel is a lightweight container for other components that can be set to a given size and color. It also contains a layout scheme for arranging the size and position of its components. Here we are constructing a JPanel as a content pane with a BorderLayout. We put a EmptyBorder inside it to serve as a margin between the edge of the window and the components inside. We then attach the JPanel as the dialog's content pane, replacing the dialog's home-grown version.

A BorderLayout is one of the simpler layout schemes available for container objects like JPanel. A BorderLayout divides the container into five sections: North, South, East, West and Center. Components are added to the layout using the container's `add` method, specifying the component to be added and the section to which it is assigned. Building a component like our dialog window involves building a set of nested containers and specifying the location of each of their member components. We have taken the first step by creating a JPanel as the dialog's content pane.

### Create the Text Fields

    // add the text fields
    fieldPanel = new JPanel(new GridLayout(4, 1, 0, 6));
    prefixField = new HistoryTextField("macro.add-prefix");
    prefixLabel = new JLabel(Prefix to add:);
    suffixField = new HistoryTextField(macro.add-suffix);
    suffixLabel = new JLabel(Suffix to add:);
    fieldPanel.add(prefixLabel);
    fieldPanel.add(prefixField);
    fieldPanel.add(suffixLabel);
    fieldPanel.add(suffixField);
    content.add(fieldPanel, Center);

Next we shall create a smaller panel containing two fields for entering the prefix and suffix text and two labels identifying the input fields.

For the text fields, we will use jEdit's HistoryTextField class. It is derived from the Java Swing class JTextField. This class offers the enhancement of a stored list of prior values used as text input. When the component has input focus, the up and down keys scroll through the prior values for the variable.

To create the HistoryTextField objects we use a constructor method that takes a single parameter: the name of the tag under which history values will be stored. Here we choose names that are not likely to conflict with existing jEdit history items.

The labels that accompany the text fields are JLabel objects from the Java Swing package. The constructor we use for both labels takes the label text as a single String parameter.

We wish to arrange these four components from top to bottom, one after the other. To achieve that, we use a JPanel container object named `fieldPanel` that will be nested inside the dialog's content pane that we have already created. In the constructor for `fieldPanel`, we assign a new GridLayout with the indicated parameters: four rows, one column, zero spacing between columns (a meaningless element of a grid with only one column, but nevertheless a required parameter) and spacing of six pixels between rows. The spacing between rows spreads out the four grid elements. After the components, the panel and the layout are specified, the components are added to `fieldPanel` top to bottom, one grid cell at a time. Finally, the complete `fieldPanel` is added to the dialog's content pane to occupy the Center section of the content pane.

### Create the Buttons

    // add the buttons
    buttonPanel = new JPanel();
    buttonPanel.setLayout(new BoxLayout(buttonPanel,
        BoxLayout.X_AXIS));
    buttonPanel.setBorder(new EmptyBorder(12, 50, 0, 50));
    buttonPanel.add(Box.createGlue());
    ok = new JButton(OK);
    cancel = new JButton(Cancel);
    ok.setPreferredSize(cancel.getPreferredSize());
    dialog.getRootPane().setDefaultButton(ok);
    buttonPanel.add(ok);
    buttonPanel.add(Box.createHorizontalStrut(6));
    buttonPanel.add(cancel);
    buttonPanel.add(Box.createGlue());
    content.add(buttonPanel, South);

To create the dialog's buttons, we follow repeat the nested container pattern we used in creating the text fields. First, we create a new, nested panel. This time we use a BoxLayout that places components either in a single row or column, depending on the parameter passed to its constructor. This layout object is more flexible than a GridLayout in that variable spacing between elements can be specified easily. We put an EmptyBorder in the new panel to set margins for placing the buttons. Then we create the buttons, using a JButton constructor that specifies the button text. After setting the size of the OK button to equal the size of the Cancel button, we designate the OK button as the default button in the dialog. This causes the OK button to be outlined when the dialog if first displayed. Finally, we place the buttons side by side with a 6 pixel gap between them (for aesthetic reasons), and place the completed `buttonPanel` in the South section of the dialog's content pane.

### Register the Action Listeners

    // register this method as an ActionListener for
    // the buttons and text fields
    ok.addActionListener(this);
    cancel.addActionListener(this);
    prefixField.addActionListener(this);
    suffixField.addActionListener(this);

In order to specify the action to be taken upon clicking a button or pressing the Enter key, we must register an ActionListener for each of the four active components of the dialog - the two HistoryTextField components and the two buttons. In Java, an ActionListener is an interface - an abstract specification for a derived class to implement. The ActionListener interface contains a single method to be implemented:

public void `actionPerformed` ActionEvent e

BeanShell does not permit a script to create derived classes. However, BeanShell offers a useful substitute: a method can be used as a scripted object that can include nested methods implementing a number of Java interfaces. The method `prefixSuffixDialog()` that we are writing can thus be treated as an ActionListener object. To accomplish this, we call `addActionListener()` on each of the four components specifying `this` as the ActionListener. We still need to implement the interface. We will do that shortly.

### Make the Dialog Visible

    // locate the dialog in the center of the
    // editing pane and make it visible
    dialog.pack();
    dialog.setLocationRelativeTo(view);
    dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    dialog.setVisible(true);

Here we do three things. First, we activate all the layout routines we have established by calling the `pack()` method for the dialog as the top-level window. Next we center the dialog's position in the active jEdit `view` by calling `setLocationRelativeTo()` on the dialog. We also call the `setDefaultCloseOperation()` function to specify that the dialog box should be immediately disposed if the user clicks the close box. Finally, we activate the dialog by calling `setVisible()`with the state parameter set to `true`.

At this point we have a decent looking dialog window that doesn't do anything. Without more code, it will not respond to user input and will not accomplish any text manipulation. The remainder of the script deals with these two requirements.

### The Action Listener

    // this method will be called when a button is clicked
    // or when ENTER is pressed
    void actionPerformed(e)
    {
        if(e.getSource() != cancel)
        {
            processText();
        }
        dialog.dispose();
    }

The method `actionPerformed()` nested inside `prefixSuffixDialog()` implements the implicit ActionListener interface. It looks at the source of the ActionEvent, determined by a call to `getSource()`. What we do with this return value is straightforward: if the source is not the Cancel button, we call the `processText()` method to insert the prefix and suffix text. Then the dialog is closed by calling its `dispose()` method.

The ability to implement interfaces like ActionListener inside a BeanShell script is one of the more powerful features of the BeanShell package. this technique is discussed in the next chapter; see .

### Get the User's Input

    // this is where the work gets done to insert
    // the prefix and suffix
    void processText()
    {
        prefix = prefixField.getText();
        suffix = suffixField.getText();
        if(prefix.length() == 0 && suffix.length() == 0)
            return;
        prefixField.addCurrentToHistory();
        suffixField.addCurrentToHistory();

The method `processText()` does the work of our macro. First we obtain the input from the two text fields with a call to their `getText()` methods. If they are both empty, there is nothing to do, so the method returns. If there is input, any text in the field is added to that field's stored history list by calling `addCurrentToHistory()`. We do not need to test the `prefixField` or `suffixField` controls for `null` or empty values because `addCurrentToHistory()` does that internally.

### Call jEdit Methods to Manipulate Text

        // text manipulation begins here using calls
        // to jEdit methods
        buffer.beginCompoundEdit();
        selectedLines = textArea.getSelectedLines();
        for(i = 0; i < selectedLines.length; ++i)
        {
            offsetBOL = textArea.getLineStartOffset(
                selectedLines[i]);
            textArea.setCaretPosition(offsetBOL);
            textArea.goToStartOfWhiteSpace(false);
            textArea.goToEndOfWhiteSpace(true);
            text = textArea.getSelectedText();
            if(text == null) text = "";
            textArea.setSelectedText(prefix + text + suffix);
        }
        buffer.endCompoundEdit();
    }

The text manipulation routine loops through each selected line in the text buffer. We get the loop parameters by calling `textArea.getSelectedLines()`, which returns an array consisting of the line numbers of every selected line. The array includes the number of the current line, whether or not it is selected, and the line numbers are sorted in increasing order. We iterate through each member of the `selectedLines` array, which represents the number of a selected line, and apply the following routine:

- Get the buffer position of the start of the line (expressed as a zero-based index from the start of the buffer) by calling `textArea.getLineStartOffset(selectedLines[i])`;

- Move the caret to that position by calling `textArea.setCaretPosition()`;

- Find the first and last non-whitespace characters on the line by calling `textArea.goToStartOfWhiteSpace()` and `textArea.goToEndOfWhiteSpace()`;

  The `goTo...` methods in JEditTextArea take a single parameter which tells jEdit whether the text between the current caret position and the desired position should be selected. Here, we call `textArea.goToStartOfWhiteSpace(false)` so that no text is selected, then call `textArea.goToEndOfWhiteSpace(true)` so that all of the text between the beginning and ending whitespace is selected.

- Retrieve the selected text by storing the return value of `textArea.getSelectedText()` in a new variable `text`.

  If the line is empty, `getSelectedText()` will return `null`. In that case, we assign an empty string to `text` to avoid calling methods on a null object.

- Change the selected text to `prefix + text + suffix` by calling `textArea.setSelectedText()`. If there is no selected text (for example, if the line is empty), the prefix and suffix will be inserted without any intervening characters.

Compound edits

Note the `beginCompoundEdit()` and `endCompoundEdit()` calls. These ensure that all edits performed between the two calls can be undone in one step. Normally, jEdit automatically wraps a macro call in these methods; however if the macro shows a non-modal dialog box, as far as jEdit is concerned the macro has finished executing by the time the dialog is shown, since control returns to the event dispatch thread.

If you do not understand this, don't worry; just keep it in mind if your macro needs to show a non-modal dialog box for some reason; Most macros won't.

### The Main Routine

    // this single line of code is the script's main routine
    prefixSuffixDialog();

The call to `prefixSuffixDialog()`is the only line in the macro that is not inside an enclosing block. BeanShell treats such code as a top-level `main` method and begins execution with it.

Our analysis of `Add_Prefix_and_Suffix.bsh` is now complete. In the next section, we look at other ways in which a macro can obtain user input, as well as other macro writing techniques.