/* Copyright 2006 by Sean Luke and George Mason University Licensed under the Academic Free License version 3.0 See the file "LICENSE" for more information */ package sim.util.gui; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.border.*; import java.awt.*; import java.util.*; import sim.util.*; /** A simple class designed to allow the user to modify a property in the form of a string, number, boolean value, or option. PropertyField lets you control the values which the user sets by subclassing the class and overriding the newValue(val) method filters all newly user-set values and "corrects" them. Programmatically set values (by calling setValue(...)) are not filtered through newValue by default. If you need to filter, you should do setValue(newValue(val)); <p>You can optionally specify how the string will be presented to the user: as a text field, as a text field with a slider (requiring certain numerical constraints on the text field), as a list of options (also requiring certain numerical constraints), as a check box (requiring the string to hold boolean values ("true" or "false"), or as a read-only field with a button to press (which in turn calls the viewProperty() method, which you may override). <p>The specifics about how to present the user with these options is described in the constructor documentation and in the documentation for setValues(...). <p>PropertyFields can also be set to be either read-only or read/write by the user. When the user edits a read/write PropertyField, the text field changes color. If the user then presses RETURN, the result is submitted to newValue(...). If the user presses ESCAPE, the result is cancelled and reset. */ public class PropertyField extends JComponent { public JComboBox list = new JComboBox(); public JTextField valField = new JTextField(); public JCheckBox checkField = new JCheckBox(); public JButton viewButton = new JButton("View"); // optionally displayed instead of valField (array or Object) public JLabel viewLabel = new JLabel(); public JLabel optionalLabel = new JLabel(); static final int SLIDER_MAX = 800000; static final int SLIDER_WIDTH = 80; public JSlider slider = new JSlider(0,SLIDER_MAX) { public Dimension getMaximumSize() { return new Dimension(SLIDER_WIDTH, super.getMaximumSize().height); } public Dimension getPreferredSize() { return getMaximumSize(); } }; public Border valFieldBorder; public Border emptyBorder; public String currentValue; public boolean isReadWrite; public Object domain; public int displayState; public static final int SHOW_CHECKBOX = 0; public static final int SHOW_TEXTFIELD = 1; public static final int SHOW_VIEWBUTTON = 2; public static final int SHOW_SLIDER = 3; public static final int SHOW_LIST = 4; public Color defaultColor; public Color editedColor = new Color(225,225,255); public void setEditedColor(Color c) { editedColor = c; } public Color getEditedColor() { return editedColor; } /** Commits to the current setting of the propertyField, filtering it through newValue. */ public void submit() { if (edited) { setValue(newValue( valField.getText() )); } } /** Reverts the property field to its previous string value WITHOUT calling newValue() */ public void update() { setValue(getValue()); } boolean edited = false; void setEdited(boolean edited) { this.edited = edited; if (edited) { valField.setBackground(editedColor); } else { valField.setBackground(isReadWrite ? defaultColor : checkField.getBackground()); } } public KeyListener listener = new KeyListener() { public void keyReleased(KeyEvent keyEvent) { } public void keyTyped(KeyEvent keyEvent) { } public void keyPressed(KeyEvent keyEvent) { if (keyEvent.getKeyCode() == KeyEvent.VK_ENTER) { submit(); } else if (keyEvent.getKeyCode() == KeyEvent.VK_ESCAPE) // reset { update(); } else { setEdited(true); } } }; public ActionListener checkListener = new ActionListener() { public void actionPerformed ( ActionEvent e ) { setValue(newValue( new Boolean(checkField.isSelected()).toString() )); } }; public ActionListener viewButtonListener = new ActionListener() { public void actionPerformed ( ActionEvent e ) { viewProperty(); } }; public FocusAdapter focusAdapter = new FocusAdapter() { public void focusLost ( FocusEvent e ) { submit(); } }; boolean sliding = false; public ChangeListener sliderListener = new ChangeListener() { public void stateChanged (ChangeEvent e) { if (domain != null && domain instanceof Interval) { double d = 0; Interval domain = (Interval)(PropertyField.this.domain); int i = slider.getValue(); if (domain.isDouble()) { double min = domain.getMin().doubleValue(); double max = domain.getMax().doubleValue(); d = (i / (double)SLIDER_MAX) * (max - min) + min; } else // long { long min = domain.getMin().longValue(); long max = domain.getMax().longValue(); d = (double)((long)((i / (double)SLIDER_MAX) * (max - min) + min)); // floor to an integer value } sliding = true; setValue(newValue("" + d)); sliding = false; } } }; public ActionListener listListener = new ActionListener() { public void actionPerformed ( ActionEvent e ) { if (!settingList) setValue(newValue(""+list.getSelectedIndex())); } }; boolean settingList = false; /** Sets the value, not filtering it through newValue(val) first. */ public void setValue(String val) { switch(displayState) { case SHOW_SLIDER: setEdited(false); if (!sliding) { slide(val); } valField.setText(val); break; case SHOW_TEXTFIELD: setEdited(false); valField.setText(val); break; case SHOW_CHECKBOX: if(val!=null && val.equals("true")) checkField.setSelected(true); else checkField.setSelected(false); break; case SHOW_VIEWBUTTON: viewLabel.setText(val); break; case SHOW_LIST: settingList = true; try { list.setSelectedIndex(Integer.parseInt(val)); } catch (Exception e) { settingList = false; throw new RuntimeException(""+e); } settingList = false; break; default: break; } currentValue = val; } void slide(String val) { try { if (domain instanceof Interval) { Interval domain = (Interval)(this.domain); double d = Double.parseDouble(val); double min = domain.getMin().doubleValue(); double max = domain.getMax().doubleValue(); int i = (int)((d - min) / (max - min) * SLIDER_MAX); slider.setValue(i); } } catch (Exception e) { } } /** Returns the most recently set value. */ public String getValue() { return currentValue; } /** Constructs a PropertyField as just a writeable, empty text field. */ public PropertyField() { this(null,"",true); } /** Constructs a PropertyField as a writeable text field with the provided initial value. */ public PropertyField(String initialValue) { this(null,initialValue,true); } /** Constructs a PropertyField as a text field with the provided initial value, either writeable or not. */ public PropertyField(String initialValue, boolean isReadWrite) { this(null,initialValue,isReadWrite); } /** Constructs a labelled PropertyField as a writeable text field with the provided initial value. */ public PropertyField(String label, String initialValue) { this(label,initialValue,true); } /** Constructs a labelled PropertyField as a text field with the provided initial value, either writeable or not. */ public PropertyField(String label, String initialValue, boolean isReadWrite) { this(label,initialValue,isReadWrite, null, SHOW_TEXTFIELD); } /** Constructs a PropertyField with an optional label, an initial value, a "writeable" flag, an optional domain (for the slider and list options), and a display form (checkboxes, view buttons, text fields, sliders, or lists). <ul> <li>If show is SHOW_CHECKBOX, a checkbox will be shown (expecting "true" and "false" string values); pass in null for domain. <li>If show is SHOW_VIEWBUTTON, a view button will be shown (expecting a true object); pass in null for domain. <li>If show is SHOW_TEXTFIELD, a textfield will be shown; pass in null for domain. <li>If show is SHOW_SLIDER, both a textfield and a slider will be shown; the initialValue must be a number, and domain must be a sim.util.Interval. In this case, newValue(...) will be passed a String holding a number in the Interval range and must return a number. PropertyField will automatically make certain that the numbers are integral or real-valued; you do not need to check this so long as the Interval returns Longs or Doubles respectively. If isReadWrite is false, then the slider is not shown -- only the textfield. <li>If show is SHOW_LIST, a list will be shown; the initialValue must be an integer specifying the number in the list, and domain must be an array of Objects (strings, whatnot) or a java.util.List providing the objects in the list. In this case, newValue(...) will be passed a String holding a number; that number is the index in the list which the user has checked. newValue(...) must also return a String with the desired index for the list to be set to. */ public PropertyField(String label, String initialValue, boolean isReadWrite, Object domain, int show) { // create object setLayout(new BorderLayout()); add(optionalLabel,BorderLayout.WEST); valFieldBorder = valField.getBorder(); Insets i = valFieldBorder.getBorderInsets(valField); emptyBorder = new EmptyBorder(i.top,i.left,i.bottom,i.right); defaultColor = valField.getBackground(); valField.addKeyListener(listener); valField.addFocusListener(focusAdapter); checkField.addActionListener(checkListener); viewButton.addActionListener(viewButtonListener); slider.addChangeListener(sliderListener); list.addActionListener(listListener); // quaquaify viewButton.putClientProperty("Quaqua.Button.style","square"); // set values setValues(label, initialValue, isReadWrite, domain, show); } /* Resets a PropertyField with an optional label, an initial value, a "writeable" flag, an optional domain (for the slider and list options), and a display form (checkboxes, view buttons, text fields, sliders, or lists). <ul> <li>If show is SHOW_CHECKBOX, a checkbox will be shown (expecting "true" and "false" string values); pass in null for domain. <li>If show is SHOW_VIEWBUTTON, a view button will be shown (expecting a true object); pass in null for domain. <li>If show is SHOW_TEXTFIELD, a textfield will be shown; pass in null for domain. <li>If show is SHOW_SLIDER, both a textfield and a slider will be shown; the initialValue must be a number, and domain must be a sim.util.Interval. In this case, newValue(...) will be passed a String holding a number in the Interval range and must return a number. PropertyField will automatically make certain that the numbers are integral or real-valued; you do not need to check this so long as the Interval returns Longs or Doubles respectively. If isReadWrite is false, then the slider is not shown -- only the textfield. <li>If show is SHOW_LIST, a list will be shown; the initialValue must be an integer specifying the number in the list, and domain must be an array of Objects (strings, whatnot) or a java.util.List providing the objects in the list. In this case, newValue(...) will be passed a String holding a number; that number is the index in the list which the user has checked. newValue(...) must also return a String with the desired index for the list to be set to. */ public void setValues(String label, String initialValue, boolean isReadWrite, Object domain, int show) { this.domain = domain; removeAll(); add(optionalLabel,BorderLayout.WEST); // some conversions if (show==SHOW_SLIDER && !isReadWrite) show = SHOW_TEXTFIELD; if (domain !=null && domain.getClass().isArray()) { domain = Arrays.asList((Object[])domain); } displayState = show; switch(displayState) { case SHOW_SLIDER: JPanel p = new JPanel(); p.setLayout(new BorderLayout()); p.add(valField, BorderLayout.CENTER); if (isReadWrite && domain!=null && domain instanceof Interval) p.add(slider, BorderLayout.WEST); add(p,BorderLayout.CENTER); break; case SHOW_TEXTFIELD: add(valField, BorderLayout.CENTER); break; case SHOW_CHECKBOX: add(checkField, BorderLayout.CENTER); break; case SHOW_VIEWBUTTON: add(viewLabel, BorderLayout.CENTER); add(viewButton, BorderLayout.WEST); break; case SHOW_LIST: if (domain != null && domain instanceof java.util.List) { settingList = true; list.setEditable(false); list.setModel(new DefaultComboBoxModel(new Vector((java.util.List)domain))); add(list,BorderLayout.CENTER); list.setEnabled(isReadWrite); settingList = false; } break; default: break; } revalidate(); repaint(); currentValue = initialValue; optionalLabel.setText(label); checkField.setEnabled(isReadWrite); valField.setEditable(isReadWrite); valField.setBorder(isReadWrite? valFieldBorder : emptyBorder); this.isReadWrite = isReadWrite; setValue(currentValue); } /** Override this to be informed when a new value has been set. The return value should be the value you want the display to show instead. */ public String newValue(String newValue) { return newValue; } /** Override this to be informed when a property is to be viewed in its own inspector because the user pressed the "view" button. */ public void viewProperty() { } public void setToolTipText(String text) { super.setToolTipText(text); valField.setToolTipText(text); checkField.setToolTipText(text); optionalLabel.setToolTipText(text); viewButton.setToolTipText(text); viewLabel.setToolTipText(text); slider.setToolTipText(text); list.setToolTipText(text); } public Dimension getMinimumSize() { Dimension s = super.getMinimumSize(); s.height = valField.getMinimumSize().height; return s; } public Dimension getPreferredSize() { Dimension s = super.getPreferredSize(); s.height = valField.getPreferredSize().height; return s; } }