/* * Query dialog. * * Copyright (c) 1998-2007 The Regents of the University of California. All * rights reserved. Permission is hereby granted, without written agreement and * without license or royalty fees, to use, copy, modify, and distribute this * software and its documentation for any purpose, provided that the above * copyright notice and the following two paragraphs appear in all copies of * this software. * * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT * OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF * CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN * "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE * MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. * * PT_COPYRIGHT_VERSION_2 COPYRIGHTENDKEY */ package ptolemy.gui; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.io.File; import java.io.IOException; import java.net.URI; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.StringTokenizer; import java.util.Vector; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JColorChooser; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JPasswordField; import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JToggleButton; import javax.swing.ScrollPaneConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.plaf.basic.BasicComboBoxEditor; // Avoid importing any packages from ptolemy.* here so that we // can ship Ptplot. // //////////////////////////////////////////////////////////////////////// // // Query /** * Create a query with various types of entry boxes and controls. Each type of * entry box has a colon and space appended to the end of its label, to ensure * uniformity. Here is one example of creating a query with a radio button: * * <pre> * query = new Query(); * getContentPane().add(query); * String[] options = { "water", "soda", "juice", "none" }; * query.addRadioButtons("radio", "Radio buttons", options, "water"); * </pre> * * @author Edward A. Lee, Manda Sutijono, Elaine Cheong * @version $Id: Query.java,v 1.127 2007/12/16 07:29:47 cxh Exp $ * @since Ptolemy II 0.3 * @Pt.ProposedRating Yellow (eal) * @Pt.AcceptedRating Red (eal) */ public class Query extends JPanel { private static final long serialVersionUID = 1L; /** * Construct a panel with no entries in it. */ public Query() { _grid = new GridBagLayout(); _constraints = new GridBagConstraints(); _constraints.fill = GridBagConstraints.HORIZONTAL; // If the next line is commented out, then the PtolemyApplet // model parameters will have an entry that is less than one // character wide unless the window is made to be fairly large. _constraints.weightx = 1.0; _constraints.anchor = GridBagConstraints.NORTHWEST; _entryPanel.setLayout(_grid); // It's not clear whether the following has any real significance... // _entryPanel.setOpaque(true); setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); _entryPanel.setAlignmentX(Component.LEFT_ALIGNMENT); // Add a message panel into which a message can be placed using // setMessage(). _messageArea = new JTextArea(""); _messageArea.setFont(new Font("SansSerif", Font.PLAIN, 12)); _messageArea.setEditable(false); _messageArea.setLineWrap(true); _messageArea.setWrapStyleWord(true); // It seems like setLineWrap is somewhat broken. Really, // setLineWrap works best with scrollbars. We have // a couple of choices: use scrollbars or hack in something // that guesses the number of lines. Note that to // use scrollbars, the tutorial at // http://java.sun.com/docs/books/tutorial/uiswing/components/simpletext.html#textarea // suggests: "If you put a text area in a scroll pane, be // sure to set the scroll pane's preferred size or use a // text area constructor that sets the number of rows and // columns for the text area." _messageArea.setBackground(null); _messageArea.setAlignmentX(Component.LEFT_ALIGNMENT); _messageScrollPane = new JScrollPane(_messageArea); _messageScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); // Get rid of the border. _messageScrollPane.setBorder(BorderFactory.createEmptyBorder()); _messageScrollPane.getViewport().setBackground(null); // Useful for debugging: // _messageScrollPane.setBorder( // BorderFactory.createLineBorder(Color.pink)); // We add the _messageScrollPane when we first use it. _entryScrollPane = new JScrollPane(_entryPanel); // Get rid of the border. _entryScrollPane.setBorder(BorderFactory.createEmptyBorder()); _entryScrollPane.getViewport().setBackground(null); _entryScrollPane.setBackground(null); add(_entryScrollPane); // Setting the background to null allegedly means it inherits the // background color from the container. _entryPanel.setBackground(null); } // ///////////////////////////////////////////////////////////////// // // public methods //// /** * Create an on-off check box. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param defaultValue * The default value (true for on). */ public void addCheckBox(String name, String label, boolean defaultValue) { JLabel lbl = new JLabel(label + ": "); lbl.setBackground(_background); JCheckBox checkbox = new JCheckBox(); checkbox.setBackground(_background); checkbox.setOpaque(false); checkbox.setSelected(defaultValue); _addPair(name, lbl, checkbox, checkbox); // Add the listener last so that there is no notification // of the first value. checkbox.addItemListener(new QueryItemListener(name)); } /** * Create an uneditable choice menu. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param values * The list of possible choices. * @param defaultChoice * Default choice. */ public void addChoice(String name, String label, String[] values, String defaultChoice) { addChoice(name, label, values, defaultChoice, false); } /** * Create a choice menu. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param values * The list of possible choices. * @param defaultChoice * Default choice. * @param editable * True if an arbitrary choice can be entered, in addition to the * choices in values. */ public void addChoice(String name, String label, String[] values, String defaultChoice, boolean editable) { addChoice(name, label, values, defaultChoice, editable, Color.white, Color.black); } /** * Create a choice menu. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param values * The list of possible choices. * @param defaultChoice * Default choice. * @param editable * True if an arbitrary choice can be entered, in addition to the * choices in values. * @param background * The background color for the editable part. * @param foreground * The foreground color for the editable part. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public void addChoice(String name, String label, String[] values, String defaultChoice, boolean editable, final Color background, final Color foreground) { JLabel lbl = new JLabel(label + ": "); lbl.setBackground(_background); JComboBox combobox = new JComboBox(values); combobox.setEditable(editable); // NOTE: Typical of Swing, the following does not set // the background color. So we have to specify a // custom editor. #$(#&$#(@#!! // combobox.setBackground(background); combobox.setEditor(new BasicComboBoxEditor() { @Override public Component getEditorComponent() { Component result = super.getEditorComponent(); result.setBackground(background); result.setForeground(foreground); return result; } }); combobox.setSelectedItem(defaultChoice); _addPair(name, lbl, combobox, combobox); // Add the listener last so that there is no notification // of the first value. combobox.addItemListener(new QueryItemListener(name)); } /** * Create a ColorChooser. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param defaultColor * The default color to use. */ public void addColorChooser(String name, String label, String defaultColor) { JLabel lbl = new JLabel(label + ": "); lbl.setBackground(_background); QueryColorChooser colorChooser = new QueryColorChooser(name, defaultColor); _addPair(name, lbl, colorChooser, colorChooser); } /** * Create a simple one-line text display, a non-editable value that is set * externally using the setDisplay() method. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param theValue * Default string to display. */ public void addDisplay(String name, String label, String theValue) { JLabel lbl = new JLabel(label + ": "); lbl.setBackground(_background); // NOTE: JLabel would be a reasonable choice here, but at // least in the current version of swing, JLabel.setText() does // not work. JTextArea displayField = new JTextArea(theValue, 1, 10); displayField.setEditable(false); displayField.setBackground(_background); _addPair(name, lbl, displayField, displayField); } /** * Create a FileChooser that selects files only, not directories, and has * the default colors (white in the background, black in the foreground). * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param defaultName * The default file name to use. * @param base * The URI with respect to which to give relative file names, or * null to give absolute file name. * @param startingDirectory * The directory to open the file chooser in. */ public void addFileChooser(String name, String label, String defaultName, URI base, File startingDirectory) { addFileChooser(name, label, defaultName, base, startingDirectory, true, false, Color.white, Color.black); } /** * Create a FileChooser with default colors (white in the foreground, black * in the background). * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param defaultName * The default file name to use. * @param base * The URI with respect to which to give relative file names, or * null to give absolute file name. * @param startingDirectory * The directory to open the file chooser in. * @param allowFiles * True if regular files may be chosen. * @param allowDirectories * True if directories may be chosen. */ public void addFileChooser(String name, String label, String defaultName, URI base, File startingDirectory, boolean allowFiles, boolean allowDirectories) { addFileChooser(name, label, defaultName, base, startingDirectory, allowFiles, allowDirectories, Color.white, Color.black); } /** * Create a FileChooser that selects files only, not directories. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param defaultName * The default file name to use. * @param base * The URI with respect to which to give relative file names, or * null to give absolute file name. * @param startingDirectory * The directory to open the file chooser in. * @param background * The background color for the text entry box. * @param foreground * The foreground color for the text entry box. */ public void addFileChooser(String name, String label, String defaultName, URI base, File startingDirectory, Color background, Color foreground) { addFileChooser(name, label, defaultName, base, startingDirectory, true, false, background, foreground); } /** * Create a FileChooser. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param defaultName * The default file name to use. * @param base * The URI with respect to which to give relative file names, or * null to give absolute file name. * @param startingDirectory * The directory to open the file chooser in. * @param allowFiles * True if regular files may be chosen. * @param allowDirectories * True if directories may be chosen. * @param background * The background color for the text entry box. * @param foreground * The foreground color for the text entry box. */ public void addFileChooser(String name, String label, String defaultName, URI base, File startingDirectory, boolean allowFiles, boolean allowDirectories, Color background, Color foreground) { JLabel lbl = new JLabel(label + ": "); lbl.setBackground(_background); QueryFileChooser fileChooser = new QueryFileChooser(name, defaultName, base, startingDirectory, allowFiles, allowDirectories, background, foreground); _addPair(name, lbl, fileChooser, fileChooser); } /** * Create a single-line entry box with the specified name, label, and * default value. To control the width of the box, call setTextWidth() * first. * * @param name * The name used to identify the entry (when accessing the * entry). * @param label * The label to attach to the entry. * @param defaultValue * Default value to appear in the entry box. */ public void addLine(String name, String label, String defaultValue) { addLine(name, label, defaultValue, Color.white, Color.black); } /** * Create a single-line entry box with the specified name, label, default * value, and background color. To control the width of the box, call * setTextWidth() first. * * @param name * The name used to identify the entry (when accessing the * entry). * @param label * The label to attach to the entry. * @param defaultValue * Default value to appear in the entry box. * @param background * The background color. * @param foreground * The foreground color. */ public void addLine(String name, String label, String defaultValue, Color background, Color foreground) { JLabel lbl = new JLabel(label + ": "); lbl.setBackground(_background); JTextField entryBox = new JTextField(defaultValue, _width); entryBox.setBackground(background); entryBox.setForeground(foreground); _addPair(name, lbl, entryBox, entryBox); // Add the listener last so that there is no notification // of the first value. entryBox.addActionListener(new QueryActionListener(name)); // Add a listener for loss of focus. When the entry gains // and then loses focus, listeners are notified of an update, // but only if the value has changed since the last notification. // FIXME: Unfortunately, Java calls this listener some random // time after the window has been closed. It is not even a // a queued event when the window is closed. Thus, we have // a subtle bug where if you enter a value in a line, do not // hit return, and then click on the X to close the window, // the value is restored to the original, and then sometime // later, the focus is lost and the entered value becomes // the value of the parameter. I don't know of any workaround. entryBox.addFocusListener(new QueryFocusListener(name)); } /** * Create a single-line password box with the specified name, label, and * default value. To control the width of the box, call setTextWidth() * first. A value that is entered in the password box should be accessed * using getCharArrayValue(). The value returned by stringValue() is * whatever you specify as a defaultValue. * * @param name * The name used to identify the entry (when accessing the * entry). * @param label * The label to attach to the entry. * @param defaultValue * Default value to appear in the entry box. * @since Ptolemy II 3.1 */ public void addPassword(String name, String label, String defaultValue) { addPassword(name, label, defaultValue, Color.white, Color.black); } /** * Create a single-line password box with the specified name, label, and * default value. To control the width of the box, call setTextWidth() * first. To get the value, call getCharArrayValue(). Calling * getStringValue() on a password entry will result in an error because it * is less secure to pass around passwords as Strings than as arrays of * characters. * <p> * The underlying class that is used to implement the password facility is * javax.swing.JPasswordField. For details about how to use JPasswordField, * see the <a href= * "http://java.sun.com/docs/books/tutorial/uiswing/components/passwordfield.html" * target="_top">Java Tutorial</a> * * @param name * The name used to identify the entry (when accessing the * entry). * @param label * The label to attach to the entry. * @param defaultValue * Default value to appear in the entry box. * @param background * The background color. * @param foreground * The foreground color. * @since Ptolemy II 3.1 */ public void addPassword(String name, String label, String defaultValue, Color background, Color foreground) { JLabel lbl = new JLabel(label + ": "); lbl.setBackground(_background); JPasswordField entryBox = new JPasswordField(defaultValue, _width); entryBox.setBackground(background); entryBox.setForeground(foreground); _addPair(name, lbl, entryBox, entryBox); // Add the listener last so that there is no notification // of the first value. entryBox.addActionListener(new QueryActionListener(name)); // Add a listener for loss of focus. When the entry gains // and then loses focus, listeners are notified of an update, // but only if the value has changed since the last notification. // FIXME: Unfortunately, Java calls this listener some random // time after the window has been closed. It is not even a // a queued event when the window is closed. Thus, we have // a subtle bug where if you enter a value in a line, do not // hit return, and then click on the X to close the window, // the value is restored to the original, and then sometime // later, the focus is lost and the entered value becomes // the value of the parameter. I don't know of any workaround. entryBox.addFocusListener(new QueryFocusListener(name)); } /** * Add a listener. The changed() method of the listener will be called when * any of the entries is changed. Note that "line" entries only trigger this * call when Return or Enter is pressed, or when the entry gains and then * loses the keyboard focus. Notice that the currently selected line loses * focus when the panel is destroyed, so notification of any changes that * have been made will be done at that time. That notification will occur in * the UI thread, and may be later than expected. Notification due to loss * of focus only occurs if the value of the entry has changed since the last * notification. If the listener has already been added, then do nothing. * * @param listener * The listener to add. * @see #removeQueryListener(QueryListener) */ public void addQueryListener(QueryListener listener) { if (_listeners == null) { _listeners = new Vector<QueryListener>(); } if (_listeners.contains(listener)) { return; } _listeners.add(listener); } /** * Create a bank of radio buttons. A radio button provides a list of * choices, only one of which may be chosen at a time. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param values * The list of possible choices. * @param defaultValue * Default value. */ public void addRadioButtons(String name, String label, String[] values, String defaultValue) { JLabel lbl = new JLabel(label + ": "); lbl.setBackground(_background); FlowLayout flow = new FlowLayout(); flow.setAlignment(FlowLayout.LEFT); // This must be a JPanel, not a Panel, or the scroll bars won't work. JPanel buttonPanel = new JPanel(flow); ButtonGroup group = new ButtonGroup(); QueryActionListener listener = new QueryActionListener(name); // Regrettably, ButtonGroup provides no method to find out // which button is selected, so we have to go through a // song and dance here... JRadioButton[] buttons = new JRadioButton[values.length]; for (int i = 0; i < values.length; i++) { JRadioButton checkbox = new JRadioButton(values[i]); buttons[i] = checkbox; checkbox.setBackground(_background); // The following (essentially) undocumented method does nothing... // checkbox.setContentAreaFilled(true); checkbox.setOpaque(false); if (values[i].equals(defaultValue)) { checkbox.setSelected(true); } group.add(checkbox); buttonPanel.add(checkbox); // Add the listener last so that there is no notification // of the first value. checkbox.addActionListener(listener); } _addPair(name, lbl, buttonPanel, buttons); } /** * Create a bank of buttons that provides a list of choices, any subset of * which may be chosen at a time. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param values * The list of possible choices. * @param initiallySelected * The initially selected choices, or null to indicate that none * are selected. */ public void addSelectButtons(String name, String label, String[] values, Set<String> initiallySelected) { JLabel lbl = new JLabel(label + ": "); lbl.setBackground(_background); FlowLayout flow = new FlowLayout(); flow.setAlignment(FlowLayout.LEFT); // This must be a JPanel, not a Panel, or the scroll bars won't work. JPanel buttonPanel = new JPanel(flow); QueryActionListener listener = new QueryActionListener(name); if (initiallySelected == null) { initiallySelected = new HashSet<String>(); } JRadioButton[] buttons = new JRadioButton[values.length]; for (int i = 0; i < values.length; i++) { JRadioButton checkbox = new JRadioButton(values[i]); buttons[i] = checkbox; checkbox.setBackground(_background); // The following (essentially) undocumented method does nothing... // checkbox.setContentAreaFilled(true); checkbox.setOpaque(false); if (initiallySelected.contains(values[i])) { checkbox.setSelected(true); } buttonPanel.add(checkbox); // Add the listener last so that there is no notification // of the first value. checkbox.addActionListener(listener); } _addPair(name, lbl, buttonPanel, buttons); } /** * Create a slider with the specified name, label, default value, maximum, * and minimum. * * @param name * The name used to identify the slider. * @param label * The label to attach to the slider. * @param defaultValue * Initial position of slider. * @param maximum * Maximum value of slider. * @param minimum * Minimum value of slider. * @exception IllegalArgumentException * If the desired default value is not between the minimum * and maximum. */ public void addSlider(String name, String label, int defaultValue, int minimum, int maximum) throws IllegalArgumentException { JLabel lbl = new JLabel(label + ": "); if (minimum > maximum) { int temp = minimum; minimum = maximum; maximum = temp; } if ((defaultValue > maximum) || (defaultValue < minimum)) { throw new IllegalArgumentException("Desired default " + "value \"" + defaultValue + "\" does not fall " + "between the minimum and maximum."); } JSlider slider = new JSlider(minimum, maximum, defaultValue); _addPair(name, lbl, slider, slider); slider.addChangeListener(new SliderListener(name)); } /** * Create a text area. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param theValue * The value of this text area */ public void addTextArea(String name, String label, String theValue) { addTextArea(name, label, theValue, Color.white, Color.black, _height, _width); } /** * Create a text area. * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param theValue * The value of this text area. * @param background * The background color. * @param foreground * The foreground color. */ public void addTextArea(String name, String label, String theValue, Color background, Color foreground) { addTextArea(name, label, theValue, background, foreground, _height, _width); } /** * Create a text area with the specified height and width (in characters). * * @param name * The name used to identify the entry (when calling get). * @param label * The label to attach to the entry. * @param theValue * The value of this text area. * @param background * The background color. * @param foreground * The foreground color. * @param height * The height. * @param width * The width. */ public void addTextArea(String name, String label, String theValue, Color background, Color foreground, int height, int width) { JLabel lbl = new JLabel(label + ": "); lbl.setBackground(_background); JTextArea textArea = new JTextArea(theValue, height, width); textArea.setEditable(true); textArea.setBackground(background); textArea.setForeground(foreground); QueryScrollPane textPane = new QueryScrollPane(textArea); _addPair(name, lbl, textPane, textPane); textArea.addFocusListener(new QueryFocusListener(name)); } /** * Get the current value in the entry with the given name and return as a * boolean. If the entry is not a checkbox, then throw an exception. * * @param name * The name of the entry. * @return The state of the checkbox. * @exception NoSuchElementException * If there is no item with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception IllegalArgumentException * If the entry is not a checkbox. This is a runtime * exception, so it need not be declared explicitly. */ public boolean getBooleanValue(String name) throws NoSuchElementException, IllegalArgumentException { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + "\" in the query box."); } if (result instanceof JToggleButton) { return ((JToggleButton) result).isSelected(); } else { throw new IllegalArgumentException( "Item named \"" + name + "\" is not a radio button, and hence does not have " + "a boolean value."); } } /** * Get the current value in the entry with the given name and return as an * array of characters. * <p> * If the entry is a password field, then it is recommended for strong * security that each element of the array be set to 0 after use. * * @param name * The name of the entry. * @return The state of the entry * @exception NoSuchElementException * If there is no item with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception IllegalArgumentException * If the entry type does not have a string representation * (this should not be thrown). This is a runtime exception, * so it need not be declared explicitly. * @since Ptolemy II 3.1 */ public char[] getCharArrayValue(String name) throws NoSuchElementException, IllegalArgumentException { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + "\" in the query box."); } if (result instanceof JPasswordField) { // Calling JPasswordField.getText() is deprecated return ((JPasswordField) result).getPassword(); } else { return getStringValue(name).toCharArray(); } } /** * Get the current value in the entry with the given name and return as a * double value. If the entry is not a line, then throw an exception. If the * value of the entry is not a double, then throw an exception. * * @param name * The name of the entry. * @return The value currently in the entry as a double. * @exception NoSuchElementException * If there is no item with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception NumberFormatException * If the value of the entry cannot be converted to a double. * This is a runtime exception, so it need not be declared * explicitly. * @exception IllegalArgumentException * If the entry is not a line. This is a runtime exception, * so it need not be declared explicitly. */ public double getDoubleValue(String name) throws IllegalArgumentException, NoSuchElementException, NumberFormatException { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + " \" in the query box."); } if (result instanceof JPasswordField) { // Note that JPasswordField extends JTextField, so // we should check for JPasswordField first. throw new IllegalArgumentException( "For security reasons, " + "calling getDoubleValue() on a password field is " + "not permitted. Instead, call getCharArrayValue()"); } else if (result instanceof JTextField) { return (Double.valueOf(((JTextField) result).getText())).doubleValue(); } else { throw new IllegalArgumentException("Item named \"" + name + "\" is not a text line, and hence cannot be converted " + "to a double value."); } } /** * Get the current value in the entry with the given name and return as an * integer. If the entry is not a line, choice, or slider, then throw an * exception. If it is a choice or radio button, then return the index of * the first selected item. * * @param name * The name of the entry. * @return The value currently in the entry as an integer. * @exception NoSuchElementException * If there is no item with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception NumberFormatException * If the value of the entry cannot be converted to an * integer. This is a runtime exception, so it need not be * declared explicitly. * @exception IllegalArgumentException * If the entry is not a choice, line, or slider. This is a * runtime exception, so it need not be declared explicitly. */ @SuppressWarnings("rawtypes") public int getIntValue(String name) throws IllegalArgumentException, NoSuchElementException, NumberFormatException { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + " \" in the query box."); } if (result instanceof JPasswordField) { // Note that JPasswordField extends JTextField, so // we should check for JPasswordField first. throw new IllegalArgumentException( "For security reasons, " + "calling getIntValue() on a password field is " + "not permitted. Instead, call getCharArrayValue()"); } else if (result instanceof JTextField) { return (Integer.valueOf(((JTextField) result).getText())).intValue(); } else if (result instanceof JSlider) { return ((JSlider) result).getValue(); } else if (result instanceof JComboBox) { return ((JComboBox) result).getSelectedIndex(); } else if (result instanceof JToggleButton[]) { // Regrettably, ButtonGroup gives no way to determine // which button is selected, so we have to search... JToggleButton[] buttons = (JToggleButton[]) result; for (int i = 0; i < buttons.length; i++) { if (buttons[i].isSelected()) { return i; } } // In theory, we shouldn't get here, but the compiler // is unhappy without a return. return -1; } else { throw new IllegalArgumentException("Item named \"" + name + "\" is not a text line or slider, and hence " + "cannot be converted to " + "an integer value."); } } /** * Return the preferred height, but set the width to the maximum possible * value. Currently (JDK 1.3), only BoxLayout pays any attention to * getMaximumSize(). * * @return The maximum desired size. */ @Override public Dimension getMaximumSize() { // Unfortunately, if we don't have a message, then we end up with // an empty space that is difficult to control the size of, which // requires us to set the maximum size to be the same as // the preferred size // If you change this, be sure to try applets that have both // horizontal and vertical layout. Dimension preferred = getPreferredSize(); preferred.width = Short.MAX_VALUE; return preferred; } /** * Get the current value in the entry with the given name, and return as a * String. All entry types support this. Note that this method should be * called from the event dispatch thread, since it needs to query to UI * widgets for their current values. If it is called from another thread, * there is no assurance that the value returned will be the current value. * * @param name * The name of the entry. * @return The value currently in the entry as a String. * @exception NoSuchElementException * If there is no item with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception IllegalArgumentException * If the entry type does not have a string representation * (this should not be thrown). */ @SuppressWarnings("rawtypes") public String getStringValue(String name) throws NoSuchElementException, IllegalArgumentException { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + " \" in the query box."); } if (result instanceof JTextField) { return ((JTextField) result).getText(); } else if (result instanceof QueryColorChooser) { return ((QueryColorChooser) result).getSelectedColor(); } else if (result instanceof QueryFileChooser) { return ((QueryFileChooser) result).getSelectedFileName(); } else if (result instanceof JTextArea) { return ((JTextArea) result).getText(); } else if (result instanceof JToggleButton) { // JRadioButton and JCheckButton are subclasses of JToggleButton JToggleButton toggleButton = (JToggleButton) result; if (toggleButton.isSelected()) { return "true"; } else { return "false"; } } else if (result instanceof JSlider) { return "" + ((JSlider) result).getValue(); } else if (result instanceof JComboBox) { return (String) (((JComboBox) result).getSelectedItem()); } else if (result instanceof JToggleButton[]) { // JRadioButton and JCheckButton are subclasses of JToggleButton // Regrettably, ButtonGroup gives no way to determine // which button is selected, so we have to search... JToggleButton[] buttons = (JToggleButton[]) result; StringBuffer toReturn = null; for (int i = 0; i < buttons.length; i++) { if (buttons[i].isSelected()) { if (toReturn == null) { toReturn = new StringBuffer(buttons[i].getText()); } else { toReturn.append(", " + buttons[i].getText()); } } } if (toReturn == null) { toReturn = new StringBuffer(); } return toReturn.toString(); } else if (result instanceof QueryScrollPane) { return ((QueryScrollPane) result).getText(); } else { throw new IllegalArgumentException("Query class cannot generate" + " a string representation for entries of type " + result.getClass()); } } /** * Get the preferred number of lines to be used for entry boxes created in * using addTextArea(). The preferred height is set using setTextHeight(). * * @return The preferred height in lines. * @see #addTextArea(String, String, String) * @see #setTextHeight(int) */ public int getTextHeight() { return _height; } /** * Get the preferred width in characters to be used for entry boxes created * in using addLine(). The preferred width is set using setTextWidth(). * * @return The preferred width of an entry box in characters. * @see #setTextWidth(int) */ public int getTextWidth() { return _width; } /** * Notify listeners of the current value of all entries, unless those * entries have not changed since the last notification. */ public void notifyListeners() { Iterator<String> names = _entries.keySet().iterator(); while (names.hasNext()) { String name = names.next(); _notifyListeners(name); } } /** * Remove a listener. If the listener has not been added, then do nothing. * * @param listener * The listener to remove. * @see #addQueryListener(QueryListener) */ public void removeQueryListener(QueryListener listener) { if (_listeners == null) { return; } _listeners.remove(listener); } /** * Set the value in the entry with the given name. The second argument must * be a string that can be parsed to the proper type for the given entry, or * an exception is thrown. Note that this does NOT trigger the notification * of listeners, and intended to allow a way to set the query to reflect the * current state. * * @param name * The name used to identify the entry (when calling get). * @param value * The value to set the entry to. * @exception NoSuchElementException * If there is no item with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception IllegalArgumentException * If the value does not parse to the appropriate type. */ @SuppressWarnings("rawtypes") public void set(String name, String value) throws NoSuchElementException, IllegalArgumentException { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + " \" in the query box."); } // FIXME: Surely there is a better way to do this... // We should define a set of inner classes, one for each entry type. // Currently, this has to be updated each time a new entry type // is added. if (result instanceof JTextField) { ((JTextField) result).setText(value); } else if (result instanceof JTextArea) { ((JTextArea) result).setText(value); } else if (result instanceof QueryScrollPane) { ((QueryScrollPane) result).setText(value); } else if (result instanceof JToggleButton) { // JRadioButton and JCheckButton are subclasses of JToggleButton Boolean flag = Boolean.valueOf(value); setBoolean(name, flag.booleanValue()); } else if (result instanceof JSlider) { Integer parsed = Integer.valueOf(value); ((JSlider) result).setValue(parsed.intValue()); } else if (result instanceof JComboBox) { ((JComboBox) result).setSelectedItem(value); } else if (result instanceof JToggleButton[]) { // First, parse the value, which may be a comma-separated list. Set<String> selectedValues = new HashSet<String>(); StringTokenizer tokenizer = new StringTokenizer(value, ","); while (tokenizer.hasMoreTokens()) { selectedValues.add(tokenizer.nextToken().trim()); } JToggleButton[] buttons = (JToggleButton[]) result; for (int i = 0; i < buttons.length; i++) { if (selectedValues.contains(buttons[i].getText())) { buttons[i].setSelected(true); } else { buttons[i].setSelected(false); } } } else if (result instanceof QueryColorChooser) { ((QueryColorChooser) result).setColor(value); } else if (result instanceof QueryFileChooser) { ((QueryFileChooser) result).setFileName(value); } else { throw new IllegalArgumentException( "Query class cannot set" + " a string representation for entries of type " + result.getClass()); } // Record the new value as if it was the previously notified // value. Thus, any future change from this value will trigger // notification. _previous.put(name, value); } /** * Set the value in the entry with the given name and notify listeners. The * second argument must be a string that can be parsed to the proper type * for the given entry, or an exception is thrown. * * @param name * The name used to identify the entry (when calling get). * @param value * The value to set the entry to. * @exception NoSuchElementException * If there is no item with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception IllegalArgumentException * If the value does not parse to the appropriate type. */ public void setAndNotify(String name, String value) throws NoSuchElementException, IllegalArgumentException { set(name, value); _notifyListeners(name); } /** * Set the background color for all the widgets. * * @param color * The background color. */ @Override public void setBackground(Color color) { super.setBackground(color); _background = color; // Set the background of any components that already exist. Component[] components = getComponents(); for (int i = 0; i < components.length; i++) { if (!(components[i] instanceof JTextField)) { components[i].setBackground(_background); } } } /** * Set the current value in the entry with the given name. If the entry is * not a checkbox, then throw an exception. Notify listeners that the value * has changed. * * @param name * The name of the entry. * @param value * The new value of the entry. * @exception NoSuchElementException * If there is no item with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception IllegalArgumentException * If the entry is not a checkbox. This is a runtime * exception, so it need not be declared explicitly. */ public void setBoolean(String name, boolean value) throws NoSuchElementException, IllegalArgumentException { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + "\" in the query box."); } if (result instanceof JToggleButton) { // JRadioButton and JCheckButton are subclasses of JToggleButton ((JToggleButton) result).setSelected(value); } else { throw new IllegalArgumentException( "Item named \"" + name + "\" is not a radio button, and hence does not have " + "a boolean value."); } _notifyListeners(name); } /** * Specify the number of columns to use. The default is one. If an integer * larger than one is specified here, then the queries will be arranged * using the specified number of columns. As queries are added, they are put * in the first row until that row is full. Then they are put in the second * row, etc. * * @param columns * The number of columns. */ public void setColumns(int columns) { if (columns <= 0) { throw new IllegalArgumentException("Query.setColumns() requires a strictly positive " + "argument."); } _columns = columns; } /** * Set the displayed text of an entry that has been added using addDisplay. * Notify listeners that the value has changed. * * @param name * The name of the entry. * @param value * The string to display. * @exception NoSuchElementException * If there is no entry with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception IllegalArgumentException * If the entry is not a display. This is a runtime * exception, so it need not be declared explicitly. */ public void setDisplay(String name, String value) throws NoSuchElementException, IllegalArgumentException { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + " \" in the query box."); } if (result instanceof JTextArea) { JTextArea label = (JTextArea) result; label.setText(value); } else { throw new IllegalArgumentException( "Item named \"" + name + "\" is not a display, and hence cannot be set using " + "setDisplay()."); } _notifyListeners(name); } /** * For line, display, check box, slider, radio button, or choice entries * made, if the second argument is false, then it will be disabled. * * @param name * The name of the entry. * @param value * If false, disables the entry. */ public void setEnabled(String name, boolean value) { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + " \" in the query box."); } if (result instanceof JComponent) { ((JComponent) result).setEnabled(value); } else if (result instanceof JToggleButton[]) { JToggleButton[] buttons = (JToggleButton[]) result; for (int i = 0; i < buttons.length; i++) { buttons[i].setEnabled(value); } } } /** * Set the displayed text of an item that has been added using addLine. * Notify listeners that the value has changed. * * @param name * The name of the entry. * @param value * The string to display. * @exception NoSuchElementException * If there is no item with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception IllegalArgumentException * If the entry is not a display. This is a runtime * exception, so it need not be declared explicitly. */ public void setLine(String name, String value) { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + " \" in the query box."); } if (result instanceof JTextField) { JTextField line = (JTextField) result; line.setText(value); } else { throw new IllegalArgumentException( "Item named \"" + name + "\" is not a line, and hence cannot be set using " + "setLine()."); } _notifyListeners(name); } /** * Specify a message to be displayed above the query. * * @param message * The message to display. */ public void setMessage(String message) { if (!_messageScrollPaneAdded) { _messageScrollPaneAdded = true; add(_messageScrollPane, 1); // Add a spacer. add(Box.createRigidArea(new Dimension(0, 10)), 2); } _messageArea.setText(message); // I'm not sure why we need to add 1 here? int lineCount = _messageArea.getLineCount() + 1; // Keep the line count to less than 30 lines. If // we have more than 30 lines, we get a scroll bar. if (lineCount > 30) { lineCount = 30; } _messageArea.setRows(lineCount); _messageArea.setColumns(_width); // In case size has changed. validate(); } /** * Set the position of an item that has been added using addSlider. Notify * listeners that the value has changed. * * @param name * The name of the entry. * @param value * The value to set the slider position. * @exception NoSuchElementException * If there is no item with the specified name. Note that * this is a runtime exception, so it need not be declared * explicitly. * @exception IllegalArgumentException * If the entry is not a slider. This is a runtime exception, * so it need not be declared explicitly. */ public void setSlider(String name, int value) { Object result = _entries.get(name); if (result == null) { throw new NoSuchElementException("No item named \"" + name + " \" in the query box."); } if (result instanceof JSlider) { JSlider theSlider = (JSlider) result; // Set the new slider position. theSlider.setValue(value); } else { throw new IllegalArgumentException( "Item named \"" + name + "\" is not a slider, and hence cannot be set using " + "setSlider()."); } _notifyListeners(name); } /** * Specify the preferred height to be used for entry boxes created in using * addTextArea(). If this is called multiple times, then it only affects * subsequent calls. * * @param characters * The preferred height. * @see #addTextArea(String, String, String) * @see #getTextHeight() */ public void setTextHeight(int characters) { _height = characters; } /** * Specify the preferred width to be used for entry boxes created in using * addLine(). If this is called multiple times, then it only affects * subsequent calls. * * @param characters * The preferred width. * @see #getTextWidth() */ public void setTextWidth(int characters) { _width = characters; } /** * Specify a tool tip to appear when the mouse lingers over the label. * * @param name * The name of the entry. * @param tip * The text of the tool tip. */ public void setToolTip(String name, String tip) { JLabel label = _labels.get(name); if (label != null) { label.setToolTipText(tip); } } /** * Convert the specified string to a color. The string has the form * "{r, g, b, a}", where each of the letters is a number between 0.0 and * 1.0, representing red, green, blue, and alpha. * * @param description * The description of the color, or white if any parse error * occurs. * @return A string representing the color. */ public static Color stringToColor(String description) { String[] specArray = description.split("[{},]"); float red = 0f; float green = 0f; float blue = 0f; float alpha = 1.0f; // If any exceptions occur during the attempt to parse, // then just use the default color. try { int i = 0; // Ignore any blank strings that this simple parsing produces. while (specArray[i].trim().equals("")) { i++; } if (specArray.length > i) { red = Float.parseFloat(specArray[i]); } i++; while (specArray[i].trim().equals("")) { i++; } if (specArray.length > i) { green = Float.parseFloat(specArray[i]); } i++; while (specArray[i].trim().equals("")) { i++; } if (specArray.length > i) { blue = Float.parseFloat(specArray[i]); } i++; while (specArray[i].trim().equals("")) { i++; } if (specArray.length > i) { alpha = Float.parseFloat(specArray[i]); } } catch (Exception ex) { // Ignore and use default color. } return new Color(red, green, blue, alpha); } // ///////////////////////////////////////////////////////////////// // // public variables //// /** The default height of entries created with addText(). */ public static final int DEFAULT_ENTRY_HEIGHT = 10; /** The default width of entries created with addLine(). */ public static final int DEFAULT_ENTRY_WIDTH = 30; // ///////////////////////////////////////////////////////////////// // // protected methods //// /** * Add a label and a widget to the panel. * * @param name * The name of the entry. * @param label * The label. * @param widget * The interactive entry to the right of the label. * @param entry * The object that contains user data. */ protected void _addPair(String name, JLabel label, Component widget, Object entry) { // Surely there is a better layout manager in swing... // Note that Box and BoxLayout do not work because they do not // support gridded layout. _constraints.gridwidth = 1; _constraints.insets = _leftPadding; _grid.setConstraints(label, _constraints); _entryPanel.add(label); _constraints.insets = _noPadding; if ((_columns > 1) && (((_entries.size() + 1) % _columns) != 0)) { _constraints.gridwidth = 1; } else { _constraints.gridwidth = GridBagConstraints.REMAINDER; } _grid.setConstraints(widget, _constraints); _entryPanel.add(widget); _entries.put(name, entry); _labels.put(name, label); _previous.put(name, getStringValue(name)); Dimension preferredSize = _entryPanel.getPreferredSize(); // Add some slop to the width to take in to account // the width of the vertical scrollbar. preferredSize.width += 25; // Applets seem to need this, see CT/SigmaDelta _widgetsHeight += widget.getPreferredSize().height; preferredSize.height = _widgetsHeight; Toolkit tk = Toolkit.getDefaultToolkit(); if (preferredSize.height > tk.getScreenSize().height) { // Fudge factor to keep this window smaller than the screen // height. CGSUnitBase and the Code Generator are good tests. preferredSize.height = (int) (tk.getScreenSize().height * 0.75); _entryScrollPane.setPreferredSize(preferredSize); } _entryScrollPane.setPreferredSize(preferredSize); // Call revalidate for the scrollbar. _entryPanel.revalidate(); } // ///////////////////////////////////////////////////////////////// // // protected variables //// /** * The background color as set by setBackground(). This defaults to null, * which indicates that the background is the same as the container. */ protected Color _background = null; /** Standard constraints for use with _grid. */ protected GridBagConstraints _constraints; /** Layout control. */ protected GridBagLayout _grid; /** List of registered listeners. */ protected Vector<QueryListener> _listeners; // ///////////////////////////////////////////////////////////////// // // friendly methods //// /** * Notify all registered listeners that something changed for the specified * entry, if it indeed has changed. The getStringValue() method is used to * check the current value against the previously notified value, or the * original value if there have been no notifications. * * @param name * The entry that may have changed. */ void _notifyListeners(String name) { if (_listeners != null) { String previous = _previous.get(name); String newValue = getStringValue(name); if (newValue.equals(previous)) { return; } // Store the new value to prevent repeated notification. // This must be done before listeners are notified, because // the notified listeners might do something that again triggers // notification, and we do not want that notification to occur // if the value has not changed. _previous.put(name, newValue); Enumeration<QueryListener> listeners = _listeners.elements(); while (listeners.hasMoreElements()) { QueryListener queryListener = listeners.nextElement(); queryListener.changed(name); } } } // ///////////////////////////////////////////////////////////////// // // private variables //// // The number of columns. private int _columns = 1; // The hashtable of items in the query. private final Map<String, Object> _entries = new HashMap<String, Object>(); // A panel within which the entries are placed. private final JPanel _entryPanel = new JPanel(); // A scroll pane that contains the _entryPanel. private final JScrollPane _entryScrollPane; // The number of lines in a text box. private int _height = DEFAULT_ENTRY_HEIGHT; // The hashtable of labels in the query. private final Map<String, JLabel> _labels = new HashMap<String, JLabel>(); // Left padding insets. private final Insets _leftPadding = new Insets(0, 10, 0, 0); // Area for messages. private JTextArea _messageArea = null; // A scroll pane that contains the _messageArea. private final JScrollPane _messageScrollPane; // True if we have added the _messageScrollPane private boolean _messageScrollPaneAdded = false; // No padding insets. private final Insets _noPadding = new Insets(0, 0, 0, 0); // The hashtable of previous values, indexed by entry name. private final Map<String, String> _previous = new HashMap<String, String>(); // The sum of the height of the widgets added using _addPair // If you adjust this, try the GR/Pendulum demo, which has // only one parameter. private int _widgetsHeight = 20; // The number of horizontal characters in a text box. private int _width = DEFAULT_ENTRY_WIDTH; // ///////////////////////////////////////////////////////////////// // // inner classes //// /** * Listener for "line" and radio button entries. */ class QueryActionListener implements ActionListener { public QueryActionListener(String name) { _name = name; } /** Call all registered QueryListeners. */ @Override public void actionPerformed(ActionEvent e) { _notifyListeners(_name); } private final String _name; } /** * Panel containing an entry box and color chooser. */ class QueryColorChooser extends Box implements ActionListener { private static final long serialVersionUID = 1L; public QueryColorChooser(String name, String defaultColor) { super(BoxLayout.X_AXIS); // _defaultColor = defaultColor; _entryBox = new JTextField(defaultColor, _width); JButton button = new JButton("Choose"); button.addActionListener(this); add(_entryBox); add(button); // Add the listener last so that there is no notification // of the first value. _entryBox.addActionListener(new QueryActionListener(name)); // Add a listener for loss of focus. When the entry gains // and then loses focus, listeners are notified of an update, // but only if the value has changed since the last notification. // FIXME: Unfortunately, Java calls this listener some random // time after the window has been closed. It is not even a // a queued event when the window is closed. Thus, we have // a subtle bug where if you enter a value in a line, do not // hit return, and then click on the X to close the window, // the value is restored to the original, and then sometime // later, the focus is lost and the entered value becomes // the value of the parameter. I don't know of any workaround. _entryBox.addFocusListener(new QueryFocusListener(name)); _name = name; } @Override public void actionPerformed(ActionEvent e) { // Read the current color from the text field. String spec = getSelectedColor().trim(); Color newColor = JColorChooser.showDialog(Query.this, "Choose Color", stringToColor(spec)); if (newColor != null) { float[] components = newColor.getRGBComponents(null); StringBuffer string = new StringBuffer("{"); // Use the syntax of arrays. for (int j = 0; j < components.length; j++) { string.append(components[j]); if (j < (components.length - 1)) { string.append(","); } else { string.append("}"); } } _entryBox.setText(string.toString()); _notifyListeners(_name); } } public String getSelectedColor() { return _entryBox.getText(); } public void setColor(String name) { _entryBox.setText(name); } private final JTextField _entryBox; private final String _name; // private String _defaultColor; } /** * Panel containing an entry box and file chooser. */ class QueryFileChooser extends Box implements ActionListener { private static final long serialVersionUID = 1L; public QueryFileChooser(String name, String defaultName, URI base, File startingDirectory, boolean allowFiles, boolean allowDirectories) { this(name, defaultName, base, startingDirectory, allowFiles, allowDirectories, Color.white, Color.black); } public QueryFileChooser(String name, String defaultName, URI base, File startingDirectory, boolean allowFiles, boolean allowDirectories, Color background, Color foreground) { super(BoxLayout.X_AXIS); _base = base; _startingDirectory = startingDirectory; if (!allowFiles && !allowDirectories) { throw new IllegalArgumentException("QueryFileChooser: nothing to be chosen."); } _allowFiles = allowFiles; _allowDirectories = allowDirectories; _entryBox = new JTextField(defaultName, _width); _entryBox.setBackground(background); _entryBox.setForeground(foreground); JButton button = new JButton("Browse"); button.addActionListener(this); add(_entryBox); add(button); // Add the listener last so that there is no notification // of the first value. _entryBox.addActionListener(new QueryActionListener(name)); // Add a listener for loss of focus. When the entry gains // and then loses focus, listeners are notified of an update, // but only if the value has changed since the last notification. // FIXME: Unfortunately, Java calls this listener some random // time after the window has been closed. It is not even a // a queued event when the window is closed. Thus, we have // a subtle bug where if you enter a value in a line, do not // hit return, and then click on the X to close the window, // the value is restored to the original, and then sometime // later, the focus is lost and the entered value becomes // the value of the parameter. I don't know of any workaround. _entryBox.addFocusListener(new QueryFocusListener(name)); _name = name; } @Override public void actionPerformed(ActionEvent e) { // NOTE: If the last argument is null, then choose a default dir. JFileChooser fileChooser = new JFileChooser(_startingDirectory); fileChooser.setApproveButtonText("Select"); // FIXME: The following doesn't have any effect. fileChooser.setApproveButtonMnemonic('S'); if (_allowFiles && _allowDirectories) { fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); } else if (_allowFiles && !_allowDirectories) { // This is the default. fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); } else if (!_allowFiles && _allowDirectories) { fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); } else { // Usually, we would use InternalErrorException here, // but if we do, then this package would depend on kernel.util, // which causes problems when we ship Ptplot. throw new RuntimeException("QueryFileChooser: nothing to be chosen."); } int returnValue = fileChooser.showOpenDialog(Query.this); if (returnValue == JFileChooser.APPROVE_OPTION) { if (_base == null) { // Absolute file name. try { _entryBox.setText(fileChooser.getSelectedFile().getCanonicalPath()); } catch (IOException ex) { // If we can't get a path, then just use the name. _entryBox.setText(fileChooser.getSelectedFile().getName()); } } else { // Relative file name. File selectedFile = fileChooser.getSelectedFile(); // FIXME: There is a bug here under Windows XP // at least... Sometimes, the drive ID (like c:) // is lower case, and sometimes it's upper case. // When we open a MoML file, it's upper case. // When we do "save as", it's lower case. // This despite the fact that both use the same // file browser to determine the file name. // Beats me... Consequence is that if you save as, // then the following relativize call doesn't work // until you close and reopen the file. try { selectedFile = selectedFile.getCanonicalFile(); } catch (IOException ex) { // Ignore, since we can't do much about it anyway. } URI relativeURI = _base.relativize(selectedFile.toURI()); _entryBox.setText(relativeURI.toString()); } _notifyListeners(_name); } } public String getSelectedFileName() { return _entryBox.getText(); } public void setFileName(String name) { _entryBox.setText(name); } private final URI _base; private final JTextField _entryBox; private final String _name; private final File _startingDirectory; private final boolean _allowFiles; private final boolean _allowDirectories; } /** * Listener for line entries, for when they lose the focus. */ class QueryFocusListener implements FocusListener { public QueryFocusListener(String name) { _name = name; } @Override public void focusGained(FocusEvent e) { // Nothing to do. } @Override public void focusLost(FocusEvent e) { // NOTE: Java's lame AWT has no reliable way // to take action on window closing, so this focus lost // notification is the only reliable way we have of reacting // to a closing window. If the previous // notification was an erroneous one and the value has not // changed, then no further notification occurs. // This could be a problem for some users of this class. _notifyListeners(_name); } private final String _name; } /** * Listener for "CheckBox" and "Choice" entries. */ class QueryItemListener implements ItemListener { public QueryItemListener(String name) { _name = name; } /** Call all registered QueryListeners. */ @Override public void itemStateChanged(ItemEvent e) { _notifyListeners(_name); } private final String _name; } /** Inner class to tie textArea to scroll pane. */ static class QueryScrollPane extends JScrollPane { // FindBugs suggests making this class static so as to decrease // the size of instances and avoid dangling references. private static final long serialVersionUID = 1L; public JTextArea textArea; QueryScrollPane(JTextArea c) { super(c); textArea = c; } public String getText() { String retval = textArea.getText(); return retval; } public void setText(String s) { textArea.setText(s); } } /** * Listener for changes in slider. */ class SliderListener implements ChangeListener { public SliderListener(String name) { _name = name; } /** Call all registered QueryListeners. */ @Override public void stateChanged(ChangeEvent event) { _notifyListeners(_name); } private final String _name; } }