Table of Contents
How to build a specific dialog box for an intervener
This is a complement to the doc How to write an intervention tool, it explains how to replace the automatic dialog box by a more specific one.
In this documentation, we will consider the example of the Diameter Height Age intervener, with two classes:
- capsis.extension.intervener.DHAThinner
- capsis.extension.intervener.DHAThinnerDialog
To build the dialog box:
- a new class is built in the same package to implement the dialog box,
- in the intervener, the initGUI () is changed.
Create a new class for the dialog box
In the same package than the intervener, create a class with same name and appending an extra “Dialog” .java. E.g. for DHAThinner → DHAThinnerDialog. You may copy the code below as a template and adapt it for your own case.
Before beginning
The code of the dialog is commented below step by step.
/* * Capsis 4 - Computer-Aided Projections of Strategies in Silviculture * * Copyright (C) 2000-2003 Francois de Coligny * * This library is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with this library; * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA */ package capsis.extension.intervener; import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.text.NumberFormat; import java.util.Locale; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; import jeeb.lib.util.AmapDialog; import jeeb.lib.util.Check; import jeeb.lib.util.ColumnPanel; import jeeb.lib.util.MessageDialog; import jeeb.lib.util.LinePanel; import jeeb.lib.util.Translator; import capsis.commongui.command.tmp.Helper; import capsis.defaulttype.Tree; /** * This dialog box is used to set DHAThinner parameters in interactive context. * * @author F. de Coligny - march 2002 */ public class DHAThinnerDialog extends AmapDialog implements ActionListener { // 24.6.2002 - fc - bug correction (phd) : getMin () and getMax () returned int values, now they // return float. DHAThinner version passed to 1.1.
Depending on the compatibility of this extension (see the matchWith () method in the intervener class, e.g. DHAThinner), it should be written in different packages:
- If the extension is generic (can work for several Capsis modules), it should go into the package names capsis.extension.intervener and should be distributed under the LGPL license (free software, copy the license comment at the top.
- If the extension is specific to some module named somemodule, it should be put in a package named somemodule.extension.intervener and the distribution license is the choice of the author (maybe do not copy the LGPL license text at the top).
The class comment should explain clearly what the class does and the @author tag is required (author's name and 'month year').
The dialog class should extend AmapDialog and implement ActionListener (to manage the responses to button clicks, see below).
private NumberFormat nf; private DHAThinner thinner; private ButtonGroup group1; private JRadioButton dbh; private JRadioButton height; private JRadioButton age; private JTextField min; private JTextField max; protected JButton ok; protected JButton cancel; protected JButton help;
The instance variables (the main variables of the dialog) :
- The reference to the intervener which configuration this dialog is for
- Widgets to get the user values (all beginning by J…, part of the java swing widgets system)
- JTextField to get from the user (or show) a number or String value
- JCheckBox to activate / desactivate an option
- JRadioButtons to choose one single option among several proposals (they must be added in a ButtonGroup)
- JButtons to open other dialogs or validate this dialog
- Additional variables, e.g. a NumberFormat to format numbers nicely in textfields (e.g. 1.1768002 → 1.18)
/** * Constructor */ public DHAThinnerDialog (DHAThinner thinner) { super (); this.thinner = thinner; // To show numbers in a nice way nf = NumberFormat.getInstance (Locale.ENGLISH); nf.setGroupingUsed (false); nf.setMaximumFractionDigits (3); createUI (); presetMinMax (); setTitle (Translator.swap ("DHAThinnerDialog")); setModal (true); // location is set by the AmapDialog superclass pack (); // uses component's preferredSize show (); }
The constructor should accept one parameter (at least one, more is possible): the intervener under configuration.
It first calls the constructor of the superclass (AmapDialog), then memorizes the intervener in its instance variable and prepares the dialog:
- create and tune the NumberFormat
- call the createUI () method to build the dialog contents (createUI () is generally long, placed at the source bottom for convenience, see lower)
- optionally run some 'first time operations' (presetMinMax ())
- setTitle (something through the Translator)
- setModal (true): nothing can be done outside the dialog until it is closed, needed here
- pack (): calculate the dialog size according to the components added in createUI ()
- show (): set the dialog visible
When the dialog is set visible, the code execution is suspended to the user actions: using the widgets to configure the options and clicking on the buttons will trigger actions (see below for the linking). When the dialog is set invisible again, the code of the caller will go on again.
/** * Accessor for context. */ public int getContext () { if (dbh.isSelected ()) { return DHAThinner.DBH; } else if (height.isSelected ()) { return DHAThinner.HEIGHT; } else { return DHAThinner.AGE; } } /** * Accessor for min value. */ public float getMin () { if (min.getText ().trim ().length () == 0) { return 0; } return (float) Check.doubleValue (min.getText ().trim ()); } /** * Accessor for max value. */ public float getMax () { if (max.getText ().trim ().length () == 0) { return Float.MAX_VALUE; } return (float) Check.doubleValue (max.getText ().trim ()); }
Accessors to get the user values at the end. These accessors may return directly the values they extract from the widgets without checking for error.
Checking the user entries
/** * Action on ok button. */ private void okAction () { boolean minIsEmpty = Check.isEmpty (min.getText ().trim ()); boolean maxIsEmpty = Check.isEmpty (max.getText ().trim ()); // Checks... if (minIsEmpty && maxIsEmpty) { MessageDialog.print (this, Translator.swap ("DHAThinnerDialog.someValueIsNeeded")); return; } // Age must be an int if (age.isSelected ()) { if (!minIsEmpty && !Check.isInt (min.getText ().trim ())) { MessageDialog.print (this, Translator.swap ( "DHAThinnerDialog.ageMustBeAnInteger")); return; } if (!maxIsEmpty && !Check.isInt (max.getText ().trim ())) { MessageDialog.print (this, Translator.swap ( "DHAThinnerDialog.ageMustBeAnInteger")); return; } // Dbh and height must be doubles } else { if (!minIsEmpty && !Check.isDouble (min.getText ().trim ())) { MessageDialog.print (this, Translator.swap ( "DHAThinnerDialog.bothValuesMustBeNumbers")); return; } if (!maxIsEmpty && !Check.isDouble (max.getText ().trim ())) { MessageDialog.print (this, Translator.swap ( "DHAThinnerDialog.bothValuesMustBeNumbers")); return; } } // Min must be lower than max if (!minIsEmpty && !maxIsEmpty) { if (Check.doubleValue (min.getText ().trim ()) > Check.doubleValue (max.getText ().trim ())) { MessageDialog.print (this, Translator.swap ( "DHAThinnerDialog.minMustBeLowerThanMax")); return; } } // All has been checked successfully, set the dialog invisible // and go back to caller (will check for validity and dispose the dialog) setValidDialog (true); } /** * Action on cancel button. */ private void cancelAction () { // Set the dialog invisible // and go back to caller (will check for validity and dispose the dialog) setValidDialog (false); }
These two methods are called resp. when the user hits Ok or Cancel (see below for linking buttons to methods).
okAction (): it is imperative that this method checks all the user entries before setting the dialog valid by setValid (true). To check the user JTextFields, you may use the Check class: pass its methods the String in the JTextField and ask if it is an int, a double, what is the int or double value… See Check for more details.
For example, if we assess that the min JTextField must contain a double value :
- We can get the String in the JTextField with min.getText ().trim () (trim () removes the optional leading and trailing spaces or tabs, convenient and suggested).
- We can test if this is a decimal number with if (Check.isDouble (…)) {…}.
- If not, we must send a message with MessageDialog (e.g. “min should be a number”) and return. Returning will exit the method and the user will be able to change the value and hit ok again for a second try.
Only if all the user entries are checked correct, set the dialog valid AND invisible with setValidDialog (true).
cancelAction (): called on Cancel, sets the dialog invalid AND invisible.
/** * Someone hit a button. */ public void actionPerformed (ActionEvent evt) { if (evt.getSource () instanceof JRadioButton) { presetMinMax (); } else if (evt.getSource ().equals (ok)) { okAction (); } else if (evt.getSource ().equals (cancel)) { cancelAction (); } else if (evt.getSource ().equals (help)) { Helper.helpFor (this); } }
actionPerformed () is called each time the user clicks on a button which was registered 'this' (the reflexive reference to the dialog itself) as listener (see createUI () below).
Depending on the source of the given event, we link with some prepared methods. If some buttons are added in createUI (), some new links may be added here.
/** * Writes the available min max values in the min and max textfields as an information * for the user. */ private void presetMinMax () { if (thinner.concernedTrees == null || thinner.concernedTrees.isEmpty ()) { return; } double m = Double.MAX_VALUE; double M = -Double.MAX_VALUE; double v = 0; for (Tree t : thinner.concernedTrees) { if (dbh.isSelected ()) { v = t.getDbh (); } else if (height.isSelected ()) { v = t.getHeight (); } else if (age.isSelected ()) { v = t.getAge (); } m = Math.min (m, v); M = Math.max (M, v); } min.setText (nf.format (m)); max.setText (nf.format (M)); }
This user method updates the min and max values each time it is called (optional).
/** * Creates the dialog box user interface. */ private void createUI () { // All lines will be inserted in this main column ColumnPanel panel = new ColumnPanel (); // Choose the mode: diameter, height OR age LinePanel l1 = new LinePanel (); l1.add (new JLabel (Translator.swap ("DHAThinnerDialog.cutTreesWith") + " :")); l1.addGlue (); panel.add (l1); // Radio buttons LinePanel l10 = new LinePanel (); LinePanel l11 = new LinePanel (); LinePanel l12 = new LinePanel (); dbh = new JRadioButton (Translator.swap ("DHAThinnerDialog.dbh")); dbh.addActionListener (this); height = new JRadioButton (Translator.swap ("DHAThinnerDialog.height")); height.addActionListener (this); age = new JRadioButton (Translator.swap ("DHAThinnerDialog.age")); age.addActionListener (this); // Add the radio buttons in their group group1 = new ButtonGroup (); group1.add (dbh); group1.add (height); group1.add (age); // Choose the default selection group1.setSelected (dbh.getModel (), true); l10.add (dbh); l10.addGlue (); l11.add (height); l11.addGlue (); l12.add (age); l12.addGlue (); panel.add (l10); panel.add (l11); panel.add (l12); // Min and max fields on a single line LinePanel l21 = new LinePanel (); min = new JTextField (5); max = new JTextField (5); l21.add (new JLabel (Translator.swap ("DHAThinnerDialog.between") + " : ")); l21.add (min); l21.add (new JLabel (" " + Translator.swap ("DHAThinnerDialog.and") + " : ")); l21.add (max); l21.addStrut0 (); panel.add (l21); panel.addGlue (); // Put the main panel at the top (north) of the user interface getContentPane ().setLayout (new BorderLayout ()); getContentPane ().add (panel, BorderLayout.NORTH); // Control panel (Ok Cancel Help) LinePanel controlPanel = new LinePanel (); ok = new JButton (Translator.swap ("Shared.ok")); cancel = new JButton (Translator.swap ("Shared.cancel")); help = new JButton (Translator.swap ("Shared.help")); controlPanel.addGlue (); // adding glue first -> the buttons will be right justified controlPanel.add (ok); controlPanel.add (cancel); controlPanel.add (help); controlPanel.addStrut0 (); ok.addActionListener (this); cancel.addActionListener (this); help.addActionListener (this); // Put the control panel at the bottom (south) getContentPane ().add (controlPanel, BorderLayout.SOUTH); // Set Ok as default (see AmapDialog) setDefaultButton (ok); } }
createUI (): this method is called in the constructor before the dialog is set visible. It builds all the dialog inner user interface by creating widgets and placing them nicely.
Why are the JLabels not declared at the top like the other widgets ?
Managing the layout
To place the widgets, we use LinePanels and ColumnPanels:
- a LinePanel will show all the added widgets on a single line respecting the insertion order
- a ColumnPanel will show all the widgets in a single column respecting the insertion order
Note: here, we add several lines in a single column to get the final aspect for our dialog (see image upper).
LinePanels and ColumnPanels must be 'closed' by adding a final glue or strut0 component:
- p.addGlue () adds a variable size space
- p.addStrut0 () adds a fixed size space (default is 2 pixels)
- when adding components in a Line or ColumnPanel, strut1 components are automatically added between them (generally 4 pixels)
Finally, we use a BorderLayout to add our panels at the NORTH and SOUTH of the dialog (EAST, WEST and CENTER are also available).
Creating the widgets
All widgets must be instanciated by 'new', taking care of their titles, names…
The widgets which must be listened to must be added a listener with w.addActionListener (this);
They must be all added in some panels to appear on the user interface.
A default button may be set with setDefaultButton (b);
Translations
All texts going to the user interface must be translated by the Translator. This includes the buttons, labels, titles and error messages.
At runtime, the Translator will replace the translation keys (e.g. “DHAThinnerDialog.dbh”) by a text according to the language of the user interface (decided at Capsis launch time, e.g. capsis -l en).
Conventions for the translation keys:
- ClassName.aMeaningfulText: e.g. “DHAThinnerDialog.dbh”, “Shared.ok” (ok is a shared key in Capsis)…
- If no translation is found by the Translator, the key will be written 'as is' on the gui: we will see what translation is missing for what key in what class and the user will be able to understand what it means (Dbh, Ok…).
- The translated texts must be provided in 2 files called DHAThinner_en.properties and DHAThinner_fr.properties for english and french resp. These files are in the same package than the intervener and its dialog. They are loaded into the Translator by this kind of statement (may be found in the intervener OR dialog code):
static { Translator.addBundle ("capsis.extension.intervener.DHAThinner"); }
Adapt the initGUI () method
In the thinner class (here DHAThinner), the initGUI () method must handle the new dialog box. (i) It first open the dialog. (ii) Then the code is executed within the dialog class until it is set invisible (see the dialog class source code for setValidDialog (true or false). (iii) When the dialog is set invisible, the code continues here and we check if it was either validated or canceled by the user (dlg.isValidDialog ()). (iv) If validated, we get the parameters entered in the dialog and copy them in the intervener instance variables (here context, min and max). Finally and in any case, we dispose() the dialog (destruction and memory release).
@Override public boolean initGUI () throws Exception { // Interactive start DHAThinnerDialog dlg = new DHAThinnerDialog (this); constructionCompleted = false; if (dlg.isValidDialog ()) { // valid -> ok was hit and all checks were ok try { context = dlg.getContext (); min = dlg.getMin (); max = dlg.getMax (); constructionCompleted = true; } catch (Exception e) { constructionCompleted = false; throw new Exception ("DHAThinner (): Could not get parameters in DHAThinnerDialog", e); } } dlg.dispose (); return constructionCompleted; }
The complete code of the dialog
/* * Capsis 4 - Computer-Aided Projections of Strategies in Silviculture * * Copyright (C) 2000-2003 Francois de Coligny * * This library is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with this library; * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA */ package capsis.extension.intervener; import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.text.NumberFormat; import java.util.Locale; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; import jeeb.lib.util.AmapDialog; import jeeb.lib.util.Check; import jeeb.lib.util.ColumnPanel; import jeeb.lib.util.MessageDialog; import jeeb.lib.util.LinePanel; import jeeb.lib.util.Translator; import capsis.commongui.command.tmp.Helper; import capsis.defaulttype.Tree; /** * This dialog box is used to set DHAThinner parameters in interactive context. * * @author F. de Coligny - march 2002 */ public class DHAThinnerDialog extends AmapDialog implements ActionListener { // 24.6.2002 - fc - bug correction (phd) : getMin () and getMax () returned int values, now they // return float. DHAThinner version passed to 1.1. private NumberFormat nf; private DHAThinner thinner; private ButtonGroup group1; private JRadioButton dbh; private JRadioButton height; private JRadioButton age; private JTextField min; private JTextField max; protected JButton ok; protected JButton cancel; protected JButton help; /** * Constructor */ public DHAThinnerDialog (DHAThinner thinner) { super (); this.thinner = thinner; // To show numbers in a nice way nf = NumberFormat.getInstance (Locale.ENGLISH); nf.setGroupingUsed (false); nf.setMaximumFractionDigits (3); createUI (); presetMinMax (); setTitle (Translator.swap ("DHAThinnerDialog")); setModal (true); // location is set by the AmapDialog superclass pack (); // uses component's preferredSize show (); } /** * Accessor for context. */ public int getContext () { if (dbh.isSelected ()) { return DHAThinner.DBH; } else if (height.isSelected ()) { return DHAThinner.HEIGHT; } else { return DHAThinner.AGE; } } /** * Accessor for min value. */ public float getMin () { if (min.getText ().trim ().length () == 0) { return 0; } return (float) Check.doubleValue (min.getText ().trim ()); } /** * Accessor for max value. */ public float getMax () { if (max.getText ().trim ().length () == 0) { return Float.MAX_VALUE; } return (float) Check.doubleValue (max.getText ().trim ()); } /** * Action on ok button. */ private void okAction () { boolean minIsEmpty = Check.isEmpty (min.getText ().trim ()); boolean maxIsEmpty = Check.isEmpty (max.getText ().trim ()); // Checks... if (minIsEmpty && maxIsEmpty) { MessageDialog.print (this, Translator.swap ("DHAThinnerDialog.someValueIsNeeded")); return; } // Age must be an int if (age.isSelected ()) { if (!minIsEmpty && !Check.isInt (min.getText ().trim ())) { MessageDialog.print (this, Translator.swap ("DHAThinnerDialog.ageMustBeAnInteger")); return; } if (!maxIsEmpty && !Check.isInt (max.getText ().trim ())) { MessageDialog.print (this, Translator.swap ("DHAThinnerDialog.ageMustBeAnInteger")); return; } // Dbh and height must be doubles } else { if (!minIsEmpty && !Check.isDouble (min.getText ().trim ())) { MessageDialog.print (this, Translator.swap ("DHAThinnerDialog.bothValuesMustBeNumbers")); return; } if (!maxIsEmpty && !Check.isDouble (max.getText ().trim ())) { MessageDialog.print (this, Translator.swap ("DHAThinnerDialog.bothValuesMustBeNumbers")); return; } } // Min must be lower than max if (!minIsEmpty && !maxIsEmpty) { if (Check.doubleValue (min.getText ().trim ()) > Check.doubleValue (max.getText ().trim ())) { MessageDialog.print (this, Translator.swap ("DHAThinnerDialog.minMustBeLowerThanMax")); return; } } // All has been checked successfully, set the dialog invisible // and go back to caller (will check for validity and dispose the dialog) setValidDialog (true); } /** * Action on cancel button. */ private void cancelAction () { // Set the dialog invisible // and go back to caller (will check for validity and dispose the dialog) setValidDialog (false); } /** * Someone hit a button. */ public void actionPerformed (ActionEvent evt) { if (evt.getSource () instanceof JRadioButton) { presetMinMax (); } else if (evt.getSource ().equals (ok)) { okAction (); } else if (evt.getSource ().equals (cancel)) { cancelAction (); } else if (evt.getSource ().equals (help)) { Helper.helpFor (this); } } /** * Writes the available min max values in the min and max textfields as an information * for the user. */ private void presetMinMax () { if (thinner.concernedTrees == null || thinner.concernedTrees.isEmpty ()) { return; } double m = Double.MAX_VALUE; double M = -Double.MAX_VALUE; double v = 0; for (Tree t : thinner.concernedTrees) { if (dbh.isSelected ()) { v = t.getDbh (); } else if (height.isSelected ()) { v = t.getHeight (); } else if (age.isSelected ()) { v = t.getAge (); } m = Math.min (m, v); M = Math.max (M, v); } min.setText (nf.format (m)); max.setText (nf.format (M)); } /** * Creates the dialog box user interface. */ private void createUI () { // All lines will be inserted in this main column ColumnPanel panel = new ColumnPanel (); // Choose the mode: diameter, height OR age LinePanel l1 = new LinePanel (); l1.add (new JLabel (Translator.swap ("DHAThinnerDialog.cutTreesWith") + " :")); l1.addGlue (); panel.add (l1); // Radio buttons LinePanel l10 = new LinePanel (); LinePanel l11 = new LinePanel (); LinePanel l12 = new LinePanel (); dbh = new JRadioButton (Translator.swap ("DHAThinnerDialog.dbh")); dbh.addActionListener (this); height = new JRadioButton (Translator.swap ("DHAThinnerDialog.height")); height.addActionListener (this); age = new JRadioButton (Translator.swap ("DHAThinnerDialog.age")); age.addActionListener (this); // Add the radio buttons in their group group1 = new ButtonGroup (); group1.add (dbh); group1.add (height); group1.add (age); // Choose the default selection group1.setSelected (dbh.getModel (), true); l10.add (dbh); l10.addGlue (); l11.add (height); l11.addGlue (); l12.add (age); l12.addGlue (); panel.add (l10); panel.add (l11); panel.add (l12); // Min and max fields on a single line LinePanel l21 = new LinePanel (); min = new JTextField (5); max = new JTextField (5); l21.add (new JLabel (Translator.swap ("DHAThinnerDialog.between") + " : ")); l21.add (min); l21.add (new JLabel (" " + Translator.swap ("DHAThinnerDialog.and") + " : ")); l21.add (max); l21.addStrut0 (); panel.add (l21); panel.addGlue (); // Put the main panel at the top (north) of the user interface getContentPane ().setLayout (new BorderLayout ()); getContentPane ().add (panel, BorderLayout.NORTH); // Control panel (Ok Cancel Help) LinePanel controlPanel = new LinePanel (); ok = new JButton (Translator.swap ("Shared.ok")); cancel = new JButton (Translator.swap ("Shared.cancel")); help = new JButton (Translator.swap ("Shared.help")); controlPanel.addGlue (); // adding glue first -> the buttons will be right justified controlPanel.add (ok); controlPanel.add (cancel); controlPanel.add (help); controlPanel.addStrut0 (); ok.addActionListener (this); cancel.addActionListener (this); help.addActionListener (this); // Put the control panel at the bottom (south) getContentPane ().add (controlPanel, BorderLayout.SOUTH); // Set Ok as default (see AmapDialog) setDefaultButton (ok); } }