/* 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.text.DecimalFormat; 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 { JComboBox list = new JComboBox(); JTextField valField = new JTextField(); JCheckBox checkField = new JCheckBox(); JButton viewButton = new JButton("View"); // optionally displayed instead of valField (array or Object) JLabel viewLabel = new JLabel(); JLabel optionalLabel = new JLabel(); static final int SLIDER_MAX = 1000; static final int SLIDER_WIDTH = 80; static final int COMBO_BOX_MAX_HEIGHT = 20; // much bigger than the default JSlider slider = new JSlider(0,SLIDER_MAX) { public Dimension getMaximumSize() { return new Dimension(SLIDER_WIDTH, super.getMaximumSize().height); } public Dimension getPreferredSize() { return getMaximumSize(); } }; DecimalFormat sliderFormatter = new DecimalFormat(); // to control the slider's number of decimal places public JTextField getField() { return valField; } Border valFieldBorder; Border emptyBorder; String currentValue; boolean isReadWrite; Object domain; 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; Color defaultColor; 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()); } } 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); } } }; ActionListener checkListener = new ActionListener() { public void actionPerformed ( ActionEvent e ) { setValue(newValue( "" + checkField.isSelected() )); } }; ActionListener viewButtonListener = new ActionListener() { public void actionPerformed ( ActionEvent e ) { viewProperty(); } }; FocusAdapter focusAdapter = new FocusAdapter() { public void focusLost ( FocusEvent e ) { submit(); } }; boolean sliding = false; /* * Calculate the number of decimal places needed to show the smallest possible change for a slider. * @param low bottom of the range * @param high top of the range * @param ticks number of discrete stops within the range * @return the number of decimal places to show * @author jharrison */ int calcDecimalPlacesForInterval(double low, double high, int ticks) { double epsilon = (high - low) / (double)ticks; return (int)Math.ceil(Math.log10(1/epsilon)); } boolean ignoreEvent = false; // set to true when we're first setting the PropertyField ChangeListener sliderListener = new ChangeListener() { public void stateChanged (ChangeEvent e) { if (!ignoreEvent && domain != null && domain instanceof Interval) { double d = 0; Interval domain = (Interval)(PropertyField.this.domain); int i = slider.getValue(); String str; if (domain.isDouble()) { double min = domain.getMin().doubleValue(); double max = domain.getMax().doubleValue(); d = (i / (double)SLIDER_MAX) * (max - min) + min; sliderFormatter.setMinimumFractionDigits(calcDecimalPlacesForInterval(min, max, SLIDER_WIDTH)); str = sliderFormatter.format(d); } else // integer str = Integer.toString(i); sliding = true; setValue(newValue(str)); sliding = false; } ignoreEvent = false; // reset } }; 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: throw new RuntimeException("default case should never occur"); } 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); if (!domain.isDouble()) i = (int)d; 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); list.setMaximumRowCount(COMBO_BOX_MAX_HEIGHT); // quaquaify viewButton.putClientProperty("Quaqua.Button.style","square"); if ((domain != null) && (domain instanceof Interval)) { Interval interval = (Interval)domain; if (interval.isDouble()) { // nothing } else { slider.setMinimum(interval.getMin().intValue()); slider.setMaximum(interval.getMax().intValue()); } } sliderFormatter.setGroupingUsed(false); // no commas // set values ignoreEvent = true; // don't change the underlying data yet 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. */ 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: throw new RuntimeException("default case should never occur"); } 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; } public void setEnabled(boolean b) { super.setEnabled(b); valField.setEnabled(b); checkField.setEnabled(b); optionalLabel.setEnabled(b); viewButton.setEnabled(b); viewLabel.setEnabled(b); slider.setEnabled(b); list.setEnabled(b); } }