PageRenderTime 187ms CodeModel.GetById 173ms app.highlight 7ms RepoModel.GetById 1ms app.codeStats 1ms


XML | 666 lines | 563 code | 82 blank | 21 comment | 0 complexity | 0cc25e7dfbc8be9de4e5287c0f2b20dd MD5 | raw file
  1<!-- jEdit 4.0 Macro Guide, (C) 2001, 2002 John Gellene                       -->
  2<!-- jEdit buffer-local properties:                                     -->
  3<!-- :indentSize=1:noTabs=yes:maxLineLen=72:tabSize=2:                  -->
  4<!-- This file contains an extended discussion of a                     -->
  5<!-- dialog-based macro example "Add_Prefix_and_Suffix.bsh"             -->
  6<!-- $Id: dialog-macro.xml 4097 2002-03-17 10:21:56Z spestov $
  7 -->
  9<chapter id="dialog-macro"><title>A Dialog-Based Macro</title>
 10 <para>
 11  Now we will look at a more complicated macro which will demonstrate
 12  some useful techniques and BeanShell features.
 13 </para>
 15 <sect1 id="dialog-macro-intro"><title>Use of the Macro</title>
 16  <para>
 17    Our new example adds prefix and suffix text to a series of selected lines. This
 18    macro can be used to reduce typing for a series of text items that must be
 19    preceded and following by identical text. In Java, for example, if we are
 20    interested in making a series of calls to
 21    <function>StringBuffer.append()</function> to construct a lengthy, formatted
 22    string, we could type the parameter for each call on successive lines as follows:
 23  </para>
 34   <para>
 35    Our macro would ask for input for the common <quote>prefix</quote> and
 36    <quote>suffix</quote> to be applied to each line; in this case, the prefix is
 37    <userinput>ourStringBuffer.append(</userinput> and the suffix is
 38    <userinput>);</userinput>. After selecting these lines and
 39    running the macro, the resulting text would look like this:
 40   </para>
 50 </sect1>
 52<sect1 id="add-prefix-and-suffix"><title>Listing of the Macro</title>
 54   <para>
 55    The macro script follows. You can find it in the jEdit
 56    distribution in the <filename>Text</filename> subdirectory of the
 57    <filename>macros</filename> directory. You can also try it out by invoking
 58    <guimenu>Macros</guimenu>&gt;<guisubmenu>Text</guisubmenu>&gt;<guimenuitem>Add
 59    Prefix and Suffix</guimenuitem>.
 60   </para>
 63<!-- <title>Add_Prefix_and_Suffix.bsh</title> -->
 64<programlisting>// beginning of Add_Prefix_and_Suffix.bsh
 66<anchor id="imports"/>// import statement (see <xref linkend=
 68import javax.swing.border.*;
 70<anchor id="main-routine"/>// main routine
 71void prefixSuffixDialog()
 73<anchor id="create-dialog"/>    // create dialog object (see <xref
 74 linkend="explain-create-dialog"/>)
 75    title = <quote>Add prefix and suffix to selected lines</quote>;
 76    dialog = new JDialog(view, title, false);
 77    content = new JPanel(new BorderLayout());
 78    content.setBorder(new EmptyBorder(12, 12, 12, 12));
 79    content.setPreferredSize(new Dimension(320, 160));
 80    dialog.setContentPane(content);
 82<anchor id="fields-panel"/>    // add the text fields (see <xref linkend=
 84    fieldPanel = new JPanel(new GridLayout(4, 1, 0, 6));
 85    prefixField = new HistoryTextField(<quote>macro.add-prefix</quote>);
 86    prefixLabel = new JLabel(<quote>Prefix to add:</quote>);
 87    suffixField = new HistoryTextField(<quote>macro.add-suffix</quote>);
 88    suffixLabel = new JLabel(<quote>Suffix to add:</quote>);
 89    fieldPanel.add(prefixLabel);
 90    fieldPanel.add(prefixField);
 91    fieldPanel.add(suffixLabel);
 92    fieldPanel.add(suffixField);
 93    content.add(fieldPanel, <quote>Center</quote>);
 95<anchor id="button-panel"/>    // add a panel containing the buttons (see <xref
 96 linkend="explain-button-panel"/>)
 97    buttonPanel = new JPanel();
 98    buttonPanel.setLayout(new BoxLayout(buttonPanel,
 99        BoxLayout.X_AXIS));
100    buttonPanel.setBorder(new EmptyBorder(12, 50, 0, 50));
101    buttonPanel.add(Box.createGlue());
102    ok = new JButton(<quote>OK</quote>);
103    cancel = new JButton(<quote>Cancel</quote>);
104    ok.setPreferredSize(cancel.getPreferredSize());
105    dialog.getRootPane().setDefaultButton(ok);
106    buttonPanel.add(ok);
107    buttonPanel.add(Box.createHorizontalStrut(6));
108    buttonPanel.add(cancel);
109    buttonPanel.add(Box.createGlue());
110    content.add(buttonPanel, <quote>South</quote>);
112<anchor id="add-listeners"/>    // register this method as an ActionListener for
113    // the buttons and text fields (see <xref linkend=
115    ok.addActionListener(this);
116    cancel.addActionListener(this);
117    prefixField.addActionListener(this);
118    suffixField.addActionListener(this);
120<anchor id="set-visible"/>    // locate the dialog in the center of the
121    // editing pane and make it visible (see <xref linkend=
123    dialog.pack();
124    dialog.setLocationRelativeTo(view);
125    dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
126    dialog.setVisible(true);
128<anchor id="action-listener"/>    // this method will be called when a button is clicked
129    // or when ENTER is pressed (see <xref linkend=
131    void actionPerformed(e)
132    {
133        if(e.getSource() != cancel)
134        {
135            processText();
136        }
137        dialog.dispose();
138    }
140<anchor id="process-text"/>    // this is where the work gets done to insert
141    // the prefix and suffix (see <xref linkend=
143    void processText()
144    {
145        prefix = prefixField.getText();
146        suffix = suffixField.getText();
147        if(prefix.length() == 0 &amp;&amp; suffix.length() == 0)
148            return;
149        prefixField.addCurrentToHistory();
150        suffixField.addCurrentToHistory();
152<anchor id="jEdit-calls"/>        // text manipulation begins here using calls
153        // to jEdit methods  (see <xref linkend="explain-jedit-calls"/>)
154        buffer.beginCompoundEdit();
155        selectedLines = textArea.getSelectedLines();
156        for(i = 0; i &lt; selectedLines.length; ++i)
157        {
158            offsetBOL = textArea.getLineStartOffset(
159                selectedLines[i]);
160            textArea.setCaretPosition(offsetBOL);
161            textArea.goToStartOfWhiteSpace(false);
162            textArea.goToEndOfWhiteSpace(true);
163            text = textArea.getSelectedText();
164            if(text == null) text = &quot;&quot;;
165            textArea.setSelectedText(prefix + text + suffix);
166        }
167        buffer.endCompoundEdit();
168    }
171<anchor id="main"/>// this single line of code is the script's main routine
172// (see <xref linkend=
176// end of Add_Prefix_and_Suffix.bsh</programlisting>
180<sect1 id="macro-analysis"><title>Analysis of the Macro</title>
182<sect2 id="explain-imports"><title>Import Statements</title>
184<informalexample><programlisting>// import statement
185import javax.swing.border.*;</programlisting></informalexample>
187  <para>
188    This macro makes use of classes in the
189    <literal>javax.swing.border</literal> package, which is not
190    automatically imported. As we mentioned
191    previously (see <xref linkend="first-example" />), jEdit's implementation of
192    BeanShell causes a number of classes to be automatically imported. Classes
193    that are not automatically imported must be identified by a full qualified
194    name or be the subject of an <function>import</function> statement.
195  </para>
199<sect2 id="explain-create-dialog"><title>Create the Dialog</title>
201<informalexample><programlisting>// create dialog object
202title = <quote>Add prefix and suffix to selected lines</quote>;
203dialog = new JDialog(view, title, false);
204content = new JPanel(new BorderLayout());
205content.setBorder(new EmptyBorder(12, 12, 12, 12));
208  <para>
209    To get input for the macro, we need a dialog that provides for input of the prefix
210    and suffix strings, an <guibutton>OK</guibutton> button to perform text
211    insertion, and a <guibutton>Cancel</guibutton> button in case we change our
212    mind. We have decided to make the dialog window non-modal. This will allow us to
213    move around in the text buffer to find things we may need (including text to cut
214    and paste) while the macro is running and the dialog is visible.
215  </para>
217  <para>
218    The Java object we need is a <classname>JDialog</classname> object from
219    the Swing package. To construct one, we use the <function>new</function>
220    keyword and call a <glossterm>constructor</glossterm> function. The
221    constructor we use takes three parameters: the owner of the new dialog,
222    the title to be displayed in the dialog frame, and a
223    <classname>boolean</classname> parameter (<constant>true</constant> or
224    <constant>false</constant>) that specifies whether the dialog will be
225    modal or non-modal. We define the variable <varname>title</varname>
226    using a string literal, then use it immediately in the
227    <classname>JDialog</classname> constructor.
228  </para>
230  <para>
231    A <classname>JDialog</classname> object is a window containing a single object
232    called a <glossterm>content pane</glossterm>. The content pane in turn contains
233    the various visible components of the dialog. A
234    <classname>JDialog</classname> creates an empty content pane for itself as
235    during its construction.  However, to control the dialog's appearance
236    as much as possible, we will separately create our own content pane and
237    attach it to the <classname>JDialog</classname>. We do this by creating a
238    <classname>JPanel</classname> object. A <classname>JPanel</classname> is a
239    lightweight container for other components that can be set to a given size and
240    color. It also contains a <glossterm>layout</glossterm> scheme for arranging the
241    size and position of its components. Here we are constructing a
242    <classname>JPanel</classname> as a content pane with a
243    <classname>BorderLayout</classname>. We put a <classname>EmptyBorder</classname>
244    inside it to serve as a margin between the edge of the window and the components
245    inside. We then attach the <classname>JPanel</classname> as the dialog's content
246    pane, replacing the dialog's home-grown version.
247  </para>
249  <para>
250    A <classname>BorderLayout</classname> is one of the simpler layout
251    schemes available for container objects like <classname>JPanel</classname>.
252    A <classname>BorderLayout</classname>
253    divides the container into five sections: <quote>North</quote>,
254    <quote>South</quote>, <quote>East</quote>, <quote>West</quote> and
255    <quote>Center</quote>. Components are added to the layout using the
256    container's <function>add</function> method, specifying the component to
257    be added and the section to which it is assigned. Building a
258    component like our dialog window involves building a set of
259    nested containers and specifying the location of each of their
260    member components. We have taken the first step by creating a
261    <classname>JPanel</classname> as the dialog's content pane.
262  </para>
266<sect2 id="explain-fields-panel"><title>
267Create the Text Fields</title>
269<informalexample><programlisting>// add the text fields
270fieldPanel = new JPanel(new GridLayout(4, 1, 0, 6));
271prefixField = new HistoryTextField(&quot;macro.add-prefix&quot;);
272prefixLabel = new JLabel(<quote>Prefix to add</quote>:);
273suffixField = new HistoryTextField(<quote>macro.add-suffix</quote>);
274suffixLabel = new JLabel(<quote>Suffix to add:</quote>);
279content.add(fieldPanel, <quote>Center</quote>);</programlisting></informalexample>
281  <para>
282    Next we shall create a smaller panel containing two fields for
283    entering the prefix and suffix text and two labels identifying the
284    input fields.
285  </para>
287  <para>
288    For the text fields, we will use jEdit's <classname>HistoryTextField</classname>
289    class. It is derived from the Java Swing class
290    <classname>JTextField</classname>. This class offers the enhancement of a stored
291    list of prior values used as text input. When the component has input focus, the
292    up and down keys scroll through the prior values for the variable.
293    <!-- The prior values are stored in a file named
294    <filename>history</filename> located in the directory in which jEdit stores
295    various user data. -->
296  </para>
298  <para>
299    To create the <classname>HistoryTextField</classname> objects we use a
300    constructor method that takes a single parameter: the name of the tag
301    under which history values will be stored. Here we choose names that are
302    not likely to conflict with existing jEdit history items.
303  </para>
305  <para>
306    The labels that accompany the text fields are
307    <classname>JLabel</classname> objects from the Java Swing
308    package. The constructor we use for both labels takes the label text
309    as a single <classname>String</classname> parameter.
310  </para>
312  <para>
313    We wish to arrange these four components from top to bottom,
314    one after the other. To achieve that, we use a
315    <classname>JPanel</classname> container object named
316    <varname>fieldPanel</varname> that
317    will be nested inside the dialog's content pane that we have
318    already created. In the constructor for <varname>fieldPanel</varname>,
319    we assign a new <classname>GridLayout</classname> with the indicated
320    parameters: four rows, one column, zero spacing between columns (a
321    meaningless element of a grid with only one column, but
322    nevertheless a required parameter) and spacing of six pixels between
323    rows. The spacing between rows spreads out the four <quote>grid</quote>
324    elements. After the components, the panel and the layout are
325    specified, the components are added to <varname>fieldPanel</varname>
326    top to bottom, one <quote>grid cell</quote> at a time. Finally, the complete
327    <varname>fieldPanel</varname> is added to the dialog's content pane to
328    occupy the <quote>Center</quote> section of the content pane.
329  </para>
332<sect2 id="explain-button-panel"><title>
333Create the Buttons</title>
335<informalexample><programlisting>// add the buttons
336buttonPanel = new JPanel();
337buttonPanel.setLayout(new BoxLayout(buttonPanel,
338    BoxLayout.X_AXIS));
339buttonPanel.setBorder(new EmptyBorder(12, 50, 0, 50));
341ok = new JButton(<quote>OK</quote>);
342cancel = new JButton(<quote>Cancel</quote>);
349content.add(buttonPanel, <quote>South</quote>);</programlisting></informalexample>
351  <para>
352    To create the dialog's buttons, we follow repeat the <quote>nested container</quote>
353    pattern we used in creating the text fields.
354    First, we create a new, nested panel. This time we use a <classname>BoxLayout</classname>
355    that places components either in a single row or
356    column, depending on the parameter passed to its constructor. This layout object
357    is more flexible than a <classname>GridLayout</classname> in that variable spacing
358    between elements can be specified easily. We put an
359    <classname>EmptyBorder</classname> in the new panel to set margins for placing
360    the buttons. Then we create the buttons, using a <classname>JButton</classname>
361    constructor that specifies the button text. After setting the size of the
362    <guilabel>OK</guilabel> button to equal the size of the
363    <guilabel>Cancel</guilabel> button, we designate the <guilabel>OK</guilabel>
364    button as the default button in the dialog. This causes the
365    <guilabel>OK</guilabel> button to be outlined when the dialog if first displayed.
366    Finally, we place the buttons side by side with a 6 pixel gap between them (for aesthetic
367    reasons), and place the completed <varname>buttonPanel</varname> in the
368    <quote>South</quote> section of the dialog's content pane.
369  </para>
372<sect2 id="explain-add-listeners">
373<title>Register the Action Listeners</title>
375<informalexample><programlisting>// register this method as an ActionListener for
376// the buttons and text fields
382  <para>
383    In order to specify the action to be taken upon clicking a
384    button or pressing the <keycap>Enter</keycap> key, we must register
385    an <classname>ActionListener</classname> for each of the four active
386    components of the dialog - the two <classname>HistoryTextField</classname>
387    components and the two buttons. In Java, an
388    <classname>ActionListener</classname> is an <glossterm>interface</glossterm> - an
389    abstract specification for a derived class to implement. The
390    <classname>ActionListener</classname> interface contains a single method to
391    be implemented:
392  </para>
395 <funcprototype>
396  <funcdef>public void <function>actionPerformed</function></funcdef>
397  <paramdef>ActionEvent <parameter>e</parameter></paramdef>
398 </funcprototype>
401  <para>
402    BeanShell does not permit a script to create derived classes.
403    However, BeanShell offers a useful substitute: a method can be
404    used as a scripted object that can include nested methods implementing a
405    number of Java interfaces. The method
406    <function>prefixSuffixDialog()</function> that we are writing can thus be
407    treated as an <classname>ActionListener</classname> object. To accomplish this, we
408    call <function>addActionListener()</function> on each of the four
409    components specifying <varname>this</varname> as the
410    <classname>ActionListener</classname>. We still need to implement the
411    interface. We will do that shortly.
412  </para>
416<sect2 id="explain-set-visible">
417<title>Make the Dialog Visible</title>
419<informalexample><programlisting>// locate the dialog in the center of the
420// editing pane and make it visible
426  <para>
427    Here we do three things. First, we activate all the layout routines we have
428    established by calling the <function>pack()</function> method for the dialog as
429    the top-level window. Next we center the dialog's position in the active jEdit
430    <varname>view</varname> by calling <function>setLocationRelativeTo()</function>
431    on the dialog. We also call the <function>setDefaultCloseOperation()</function>
432    function to specify that the dialog box should be immediately disposed if the
433    user clicks the close box. Finally, we activate the dialog by calling
434    <function>setVisible()</function>with the state parameter set to
435    <constant>true</constant>.
436  </para>
438  <para>
439    At this point we have a decent looking dialog window that
440    doesn't do anything. Without more code, it will not respond to
441    user input and will not accomplish any text manipulation. The
442    remainder of the script deals with these two requirements.
443  </para>
447<sect2 id="explain-action-listener">
448<title>The Action Listener</title>
450<informalexample><programlisting>// this method will be called when a button is clicked
451// or when ENTER is pressed
452void actionPerformed(e)
454    if(e.getSource() != cancel)
455    {
456        processText();
457    }
458    dialog.dispose();
461  <para>
462    The method <function>actionPerformed()</function> nested inside
463    <function>prefixSuffixDialog()</function> implements the implicit
464    <classname>ActionListener</classname> interface. It looks at the source
465    of the <classname>ActionEvent</classname>, determined by a call to
466    <function>getSource()</function>. What we do with this return value is
467    straightforward: if the source is not the <guibutton>Cancel</guibutton> button, we
468    call the <function>processText()</function> method to insert the prefix
469    and suffix text. Then the dialog is closed by calling its
470    <function>dispose()</function> method.
471  </para>
473  <para>
474    The ability to implement interfaces like
475    <classname>ActionListener</classname> inside a BeanShell script is
476    one of the more powerful features of the BeanShell package. With an
477    <classname>ActionListener</classname> interface, which has only a
478    single method, implementation is simple.  When using other
479    interfaces with multiple methods, however, there are some details to
480    deal with that will vary depending on the version of the Java
481    platform that you are running.  These techniques are discussed in
482    the next chapter; see <xref linkend="macro-tips-BeanShell-interface" />.
483  </para>
487<sect2 id="explain-process-text">
488<title>Get the User's Input</title>
490<informalexample><programlisting>// this is where the work gets done to insert
491// the prefix and suffix
492void processText()
494    prefix = prefixField.getText();
495    suffix = suffixField.getText();
496    if(prefix.length() == 0 &amp;&amp; suffix.length() == 0)
497        return;
498    prefixField.addCurrentToHistory();
499    suffixField.addCurrentToHistory();</programlisting></informalexample>
501  <para>
502    The method <function>processText()</function> does the work of our
503    macro. First we obtain the input from the two text fields with a
504    call to their <function>getText()</function> methods. If they are both
505    empty, there is nothing to do, so the method returns. If there is
506    input, any text in the field is added to that field's stored
507    history list by calling <function>addCurrentToHistory()</function>.
508    We do not need to test the <varname>prefixField</varname> or
509    <varname>suffixField</varname> controls for <constant>null</constant>
510    or empty values because <function>addCurrentToHistory()</function>
511    does that internally.
512  </para>
516<sect2 id="explain-jedit-calls">
517<title>Call jEdit Methods to Manipulate Text</title>
519<informalexample><programlisting>    // text manipulation begins here using calls
520    // to jEdit methods
521    buffer.beginCompoundEdit();
522    selectedLines = textArea.getSelectedLines();
523    for(i = 0; i &lt; selectedLines.length; ++i)
524    {
525        offsetBOL = textArea.getLineStartOffset(
526            selectedLines[i]);
527        textArea.setCaretPosition(offsetBOL);
528        textArea.goToStartOfWhiteSpace(false);
529        textArea.goToEndOfWhiteSpace(true);
530        text = textArea.getSelectedText();
531        if(text == null) text = &quot;&quot;;
532        textArea.setSelectedText(prefix + text + suffix);
533    }
534    buffer.endCompoundEdit();
537  <para>
538    The text manipulation routine loops through each selected line
539    in the text buffer. We get the loop parameters by calling
540    <function>textArea.getSelectedLines()</function>, which returns an array
541    consisting of the line numbers of every selected line.  The array includes the
542    number of the current line, whether or not it is selected, and the line numbers
543    are sorted in increasing order.  We iterate through each member of the
544    <varname>selectedLines</varname> array, which represents the number of a
545    selected line, and apply the following routine:
546  </para>
550  <listitem>
551    <para>
552      Get the buffer position of the start of the line (expressed
553      as a zero-based index from the start of the buffer) by calling
554      <function>textArea.getLineStartOffset(selectedLines[i])</function>;
555    </para>
556  </listitem>
558  <listitem>
559    <para>
560      Move the caret to that position by calling
561      <function>textArea.setCaretPosition()</function>;
562    </para>
563  </listitem>
565  <listitem>
566    <para>
567      Find the first and last non-whitespace characters on the line
568      by calling <function>textArea.goToStartOfWhiteSpace()</function> and
569      <function>textArea.goToEndOfWhiteSpace()</function>;
570    </para>
572    <para>
573      The <function>goTo...</function> methods in
574      <classname>JEditTextArea</classname> take a single parameter which
575      tells jEdit whether the text between the current caret position and
576      the desired position should be selected. Here, we call
577      <function>textArea.goToStartOfWhiteSpace(false)</function> so that
578      no text is selected, then call
579      <function>textArea.goToEndOfWhiteSpace(true)</function> so that all of
580      the text between the beginning and ending whitespace is
581      selected.
582    </para>
583  </listitem>
585  <listitem>
586    <para>
587      Retrieve the selected text by storing the return value of
588      <function>textArea.getSelectedText()</function> in a new variable
589      <function>text</function>.
590    </para>
592    <para>
593      If the line is empty, <function>getSelectedText()</function> will
594      return <constant>null</constant>. In that case, we assign an empty
595      string to <varname>text</varname> to avoid calling methods on a
596      null object.
597    </para>
598  </listitem>
600  <listitem>
601    <para>
602      Change the selected text to <varname>prefix + text +
603      suffix</varname> by calling
604      <function>textArea.setSelectedText()</function>.
605      If there is no selected text (for example, if the line is empty),
606      the prefix and suffix will be inserted without any intervening
607      characters.
608    </para>
609  </listitem>
613<sidebar><title>Compound edits</title>
614 <para>
615  Note the <function>beginCompoundEdit()</function> and
616  <function>endCompoundEdit()</function> calls. These ensure that all edits
617  performed between the two calls can be undone in one step. Normally,
618  jEdit automatically wraps a macro call in these methods; however if
619  the macro shows a non-modal dialog box, as far as jEdit is concerned
620  the macro has finished executing by the time the dialog is shown,
621  since control returns to the event dispatch thread.
622 </para>
623 <para>
624  If you do not understand this, don't worry; just keep it in mind if
625  your macro needs to show a non-modal dialog box for some reason;
626  Most macros won't.
627 </para>
630  <!-- <para>
631    The loop routine is bracketed by calls to
632    <function>buffer.beginCompoundEdit()</function> and
633    <function>buffer.endCompoundEdit()</function>.  These methods
634    modify the way in which jEdit's undo facility performs.  Text
635    operations done between calls to these functions will be
636    treated as a single operation for undo purposes.  A single undo
637    command issued immediately after the macro completes will thus remove
638    the prefix and suffix text from all of the previously selected lines.
639  </para> -->
643<sect2 id="explain-main">
644<title>The Main Routine</title>
646<informalexample><programlisting>// this single line of code is the script's main routine
650  <para>
651    The call to <function>prefixSuffixDialog()</function>is the only line
652    in the macro that is not inside an enclosing block. BeanShell
653    treats such code as a top-level <function>main</function> method and
654    begins execution with it.
655  </para>
657  <para>
658    Our analysis of <filename>Add_Prefix_and_Suffix.bsh</filename> is now
659    complete. In the next section, we look at other ways in which a macro
660    can obtain user input, as well as other macro writing techniques.
661  </para>