// 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 the section called “The Mandatory First Example”), 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 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.
// 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.
// 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 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.
// 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.
// 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 the section called “Implementing Classes and Interfaces”.
// 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.
// 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.
// 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.