/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Blacksmith, Inc. 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, see http://www.gnu.org */ package net.wotonomy.ui.swing.components; import java.awt.AWTEventMulticaster; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.MethodDescriptor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.swing.Box; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingConstants; /** * InfoPanel uses labels and textfields (or any other component - see below) to * display a list of keys and values in a well-aligned and consistent manner, * conforming to alignment and pixel spacing in the java look and feel * design * guidelines.
*
* * Each key is displayed in a label to the left of the component that contains * the corresponding value. Each row is displayed starting at the top of the * component's available area. Each row's height is the maximum preferred height * of its components and the field itself gets as much of the width as it can, * dependent on the length of the longest label.
*
* * The values in the fields can be editable, and the current value can be * retrieved using the key - for this reason, unique keys are recommended.
*
* * As a convenience, push buttons may be placed across the bottom of the panel * in a manner similar to ButtonPanel.
*
* * The panel forwards any ActionEvents generated by the components and buttons * on it to all registered listeners.
*
* * Optionally, any component can be used instead of a textfield. However, * get/setValueForKey() and get/setEditable() may not * work for those components. Use getComponentForKey() to access * them instead. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 904 $ $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) * $ */ public class InfoPanel extends JPanel implements ActionListener { /** * Special label for an empty pair - a label and component that take up space * but are hidden from view. This might be useful for achieving certain layouts. */ public static final String HIDDEN = "(hidden)"; /** Cache for the introspectComponent method */ private static Map _method_cache = Collections.synchronizedMap(new HashMap(30)); protected Container listContainer = null; protected int hgap; // set in constructor protected int vgap; // set in constructor protected int margin; // set in constructor protected int columns; // set in constructor protected List fields = null; protected List labels = null; protected List fieldSpacers = null; protected ButtonPanel buttonPanel = null; protected boolean isEditable = true; protected String prefix; protected String postfix; protected int labelAnchor; protected int labelAlign; // protected Component marginStrut = null; // for action multicasting protected ActionListener actionListener = null; /** * Constructs an empty InfoPanel. */ public InfoPanel() { hgap = 12; // per java l&f guidelines vgap = 6; // java l&f says 11 columns = 1; // default columns margin = 0; // default margin: none prefix = ""; // default prefix: none postfix = ":"; // per java l&f guidelines fields = new ArrayList<>(); labels = new ArrayList<>(); labelAnchor = GridBagConstraints.NORTHWEST; // per java l&f guidelines (CENTER is nicer) labelAlign = SwingConstants.LEFT; // per java l&f guidelines doInitialLayout(); } /** * Constructs an InfoPanel with the specified labels each paired with a blank * textfield. * * @param labelArray An Array containing the labels in the order in which they * should appear from top to bottom. A null value produces an * empty panel. */ public InfoPanel(String[] labelArray) { this(); setLabels(labelArray); } /** * Creates a set of labels and empty textfields after first clearing all * existing components on the panel. * * @param labelArray An Array containing the labels in the order in which they * should appear from top to bottom. A null value will clear * the panel. */ public void setLabels(String[] labelArray) { removeAll(); if (labelArray == null) return; // null clears panel for (int i = 0; i < labelArray.length; i++) { addPair(labelArray[i], new JTextField()); } } /** * Retrieves the labls for the components on the panel in the order in which * they are displayed from top WIDTH bottom. These are the keys used to * reference values or to reference the components directly. * * @return An Array of Strings containing the labels. */ public String[] getLabels() { int length = fields.size(); String[] labelArray = new String[length]; for (int i = 0; i < length; i++) { labelArray[i] = ((Component) fields.get(i)).getName(); } return labelArray; } /** * Retrieves the constant used to anchor the labels in place. The default value * is GridBagConstraints.NORTHWEST. */ public int getLabelAnchor() { return labelAnchor; } /** * Sets the constant used to anchor the labels in place and reflows the layout. * * @param anAnchorConstant An anchor constant from GridBagConstraints. */ public void setLabelAnchor(int anAnchorConstant) { labelAnchor = anAnchorConstant; updateLabels(); } /** * Retrieves the constant used to align the labels in place. The default value * is GridBagConstraints.CENTER. */ public int getLabelAlignment() { return labelAlign; } /** * Sets the constant used to align the labels in place and reflows the layout. * * @param anAlignmentConstant LEFT, CENTER, or RIGHT constants from * SwingUtilities. */ public void setLabelAlignment(int anAlignmentConstant) { labelAlign = anAlignmentConstant; updateLabels(); } /** * Factory method for creating panel spacers. This implementation returns a * JPanel with opaque set to false. Override to customize. */ public JPanel createPanel() { JPanel result = new JPanel(); result.setOpaque(false); return result; } /** * This method is responsible for the initial layout of the panel. All labels * and textfields will later added to listContainer. This method is responsible * for initializing listContainer. */ protected void doInitialLayout() { listContainer = createPanel(); listContainer.setLayout(new BetterGridBagLayout()); this.setLayout(new BorderLayout()); this.add(listContainer, BorderLayout.NORTH); // listContainer.setBackground( Color.blue ); // useful for testing // this.setBackground( Color.red ); } /** * Changes the horizontal spacing between the label and the components in the * panel. Note: Assumes listContainer uses a GridBagLayout. * * @param newHgap the new spacing, in pixels. May not be negative. */ public void setHgap(int newHgap) { if (newHgap < 0) return; // may not be negative this.hgap = newHgap; updateGaps(); this.revalidate(); this.repaint(); } /** * Gets the current horizontal spacing between components. * * @return the current horizontal spacing, in pixels. */ public int getHgap() { return this.hgap; } /** * Changes the vertical spacing between components in the panel. Note: Assumes * listContainer uses a GridBagLayout. * * @param newVgap the new spacing, in pixels. May not be negative. */ public void setVgap(int newVgap) { if (newVgap < 0) return; // may not be negative this.vgap = newVgap; updateGaps(); this.revalidate(); this.repaint(); } /** * Gets the current vertical spacing between components. * * @return the current vertical spacing, in pixels. */ public int getVgap() { return this.vgap; } /** * Sets the minimum width for the labels column. This left margin will grow if * one of the labels is wider than this value. Note: assumes GridBagLayout. * * @param newMargin the new minimum margin in pixels. May not be negative. */ public void setMargin(int newMargin) { if (newMargin < 0) return; // may not be negative this.margin = newMargin; if (listContainer.getLayout() instanceof GridBagLayout) { GridBagLayout gridBag = (GridBagLayout) listContainer.getLayout(); GridBagConstraints constraints = null; Component c = null; int count = listContainer.getComponentCount(); for (int i = 0; i < count; i++) { c = listContainer.getComponent(i); constraints = gridBag.getConstraints(c); if (constraints.gridy == 0 && constraints.gridx % 2 == 0) { // if this is a label spacer // replace it with an appropriately sized // box listContainer.remove(c); listContainer.add(Box.createHorizontalStrut(this.margin), constraints); } } } this.revalidate(); this.repaint(); } /** * Gets the current minimum margin for the labels column. * * @return the current minimum margin in pixels. */ public int getMargin() { return this.margin; } /** * Sets the number of columns for the panel. Label/Component pairs will start * from the top left and fill in to the right before wrapping to the next row. * The default number of columns is one. Note: assumes GridBagLayout. * * @param newColumns the new number of columns. May not be less than one. */ public void setColumns(int newColumns) { if (newColumns < 1) return; // may not be less than one. int oldColumns = this.columns; this.columns = newColumns; if (listContainer.getLayout() instanceof GridBagLayout) { GridBagLayout gridBag = (GridBagLayout) listContainer.getLayout(); int count = listContainer.getComponentCount(); Component[] components = listContainer.getComponents(); GridBagConstraints[] constraints = new GridBagConstraints[components.length]; for (int i = 0; i < components.length; i++) { constraints[i] = gridBag.getConstraints(components[i]); } listContainer.removeAll(); for (int i = 0; i < components.length; i++) { if (constraints[i].gridy != 0) { // ignore first row which is reserved for spacers. // translate component to new position // (columns*2 accounts for two grid columns for one "actual" column) int index = (constraints[i].gridy - 1) * oldColumns * 2 + constraints[i].gridx; constraints[i].gridy = (index / (newColumns * 2)) + 1; constraints[i].gridx = index % (newColumns * 2); listContainer.add(components[i], constraints[i]); } } createSpacers(); // replace the spacers updateGaps(); } this.revalidate(); this.repaint(); } /** * Sets the vertical weight used for determining how to distribute additional * vertical space in the component. * * @param aComponent Key that exists in the layout. * @return weighty The weight of the component, or -1.0 if not found. */ public double getVerticalWeightForKey(String key) { Container c = getCompositeComponentForKey(key); if (c == null) return -1.0; if (!(listContainer.getLayout() instanceof GridBagLayout)) return -1.0; GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); GridBagConstraints gbc = layout.getConstraints(c); return gbc.weighty; } /** * Sets the vertical weight used for determining how to distribute additional * vertical space in the component. By default, all weights are zero, so each * component gets its preferred height. If any weights are specified, then * additional space is allocated to those components proportionately. * * @param aComponent Key that exists in the layout. * @param weighty The new weight. */ public void setVerticalWeightForKey(String key, double weighty) { Container c = getCompositeComponentForKey(key); if (c == null) return; if (!(listContainer.getLayout() instanceof GridBagLayout)) return; GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); GridBagConstraints gbc = layout.getConstraints(c); gbc.weighty = weighty; layout.setConstraints(c, gbc); // handle adding on-the-fly updateGaps(); this.revalidate(); this.repaint(); } /** * Gets the current number of columns. * * @return the current number of columns. */ public int getColumns() { return this.columns; } /** * Appends a label containing a key and the specified component to the bottom of * the panel. Any registered action listeners will receive action events from * the component - the key corresponding to the component will be used as the * action command. * * @param key A string that will be displayed in a label, preferrably * unique. * @param component A component that will be placed next to the label. If null, * a blank JPanel will be used. */ public void addPair(String key, Component component) { addRow(key, new Component[] { component }); } /** * Appends a label containing a key and the specified component to the bottom of * the panel. Any registered action listeners will receive action events from * the component - the key corresponding to the component will be used as the * action command. * * @param key A string that will be displayed in a label, preferrably * unique. * @param component A component that will be placed next to the label. If null, * a blank JPanel will appear. */ public void addRow(String key, Component component) { addRow(key, new Component[] { component }); } /** * Appends a label containing a key and the specified components to the bottom * of the panel. Any registered action listeners will receive action events from * the component - the key corresponding to the component will be used as the * action command. * * @param key A string that will be displayed in a label, preferrably * unique. * @param components An array of components that will be placed next to the * label. Any nulls in the list will be replaced with blank * JPanels. */ public void addRow(String key, Component[] components) { addCompositeComponent(key, makeCompositeComponent(key, components)); } /** * Appends a label containing a key and the specified components to the bottom * of the panel. Any registered action listeners will receive action events from * the components - the key corresponding to the component will be used as the * action command. * * @param key A string that will be displayed in a label, preferrably unique. * @param west A component that will appear to the left of the other * components, as wide as its preferred width and as tall as the * tallest of the other components. A null will be replaced with a * blank JPanel. * @param center A component that will appear between the other components, * taking up available space. A null will be replaced with a blank * JPanel. * @param east A component that will appear to the right of the other * components, as wide as its preferred width and as tall as the * tallest of the other components. A null will be replaced with a * blank JPanel. */ public void addRow(String key, Component west, Component center, Component east) { addCompositeComponent(key, makeCompositeComponent(key, west, center, east)); } /** * Appends a label containing a key and the specified components to the bottom * of the panel. Any registered action listeners will receive action events from * the components - the key corresponding to the component will be used as the * action command. * * @param key A string that will be displayed in a label, preferrably unique. * @param west A component that will appear to the left of the other * components, as wide as its preferred width and as tall as the * tallest of the other components. A null will be replaced with a * blank JPanel. * @param north A component that will appear above all the other components, as * tall as its preferred height and as wide as the info panel * itself. * @param center A component that will appear between the other components, * taking up available space. A null will be replaced with a blank * JPanel. * @param south A component that will appear below all the other components, as * tall as its preferred height and as wide as the info panel * itself. * @param east A component that will appear to the right of the other * components, as wide as its preferred width and as tall as the * tallest of the other components. A null will be replaced with a * blank JPanel. */ public void addRow(String key, Component west, Component north, Component center, Component south, Component east) { addCompositeComponent(key, makeCompositeComponent(key, west, north, center, south, east)); } /** * Produces a container that contains the specified components, using * GridLayout. Nulls are ignored. This implementation returns a JPanel. */ protected Container makeCompositeComponent(String key, Component[] components) { JPanel panel = createPanel(); if (components.length != 0) { panel.setLayout(new GridLayout(1, components.length, hgap, vgap)); Component c; for (int i = 0; i < components.length; i++) { c = components[i]; if (c != null) { introspectComponent(c, key); panel.add(c); } } } return panel; } /** * Produces a container that contains the specified components, using * BorderLayout. Nulls are ignored. This implementation returns a JPanel. */ protected Container makeCompositeComponent(String key, Component west, Component center, Component east) { JPanel panel = createPanel(); panel.setLayout(new BorderLayout(hgap, vgap)); if (west != null) { introspectComponent(west, key); panel.add(west, BorderLayout.WEST); } if (center != null) { introspectComponent(center, key); panel.add(center, BorderLayout.CENTER); } if (east != null) { introspectComponent(east, key); panel.add(east, BorderLayout.EAST); } return panel; } /** * Produces a container that contains the specified components, using * BorderLayout. Nulls are ignored. This implementation returns a JPanel. */ protected Container makeCompositeComponent(String key, Component west, Component north, Component center, Component south, Component east) { JPanel panel = createPanel(); panel.setLayout(new BorderLayout(hgap, vgap)); if (west != null) { introspectComponent(west, key); panel.add(west, BorderLayout.WEST); } if (north != null) { introspectComponent(north, key); panel.add(north, BorderLayout.WEST); } if (center != null) { introspectComponent(center, key); panel.add(center, BorderLayout.CENTER); } if (south != null) { introspectComponent(south, key); panel.add(south, BorderLayout.CENTER); } if (east != null) { introspectComponent(east, key); panel.add(east, BorderLayout.EAST); } return panel; } /** * Override to return a specific component to be used as a label. This * implementation calls createLabel(). */ protected Component createLabelForKey(String aKey) { return createLabel(); } /** * Provided for backwards compatibility, and called by the default * implementation of createLabelForKey. This implementation returns a JLabel. */ protected JLabel createLabel() { return new JLabel(); } /** * Appends a label containing a key and the specified component to the bottom of * the panel. Any registered action listeners will receive action events from * the component - the key corresponding to the component will be used as the * action command. * * @param key A string that will be displayed in a label, preferrably * unique. * @param component A component that will be placed next to the label. If null, * a stock JTextField will be used. */ protected void addCompositeComponent(String key, Component component) { if (key == null) { key = ""; } Component label = createLabelForKey(key); Component field = component; if (field == null) { field = new JTextField(15); // default to 15 columns } field.setName(key); // for association and reference label.setName(key); // ditto if (label instanceof JLabel) { ((JLabel) label).setHorizontalAlignment(labelAlign); ((JLabel) label).setLabelFor(field); // for accessibility } if ("".equals(key)) { setText(label, ""); } else { setText(label, prefix + key + postfix); } field.setEnabled(this.isEditable); // was: setEditable GridBagConstraints gbc = new GridBagConstraints(); if (listContainer.getComponentCount() == 0) { // we've just initialized or called removeAll createSpacers(); } gbc.gridx = (fields.size() % this.columns) * 2; gbc.gridy = (fields.size() / this.columns) + 1; // spacer is at index zero gbc.weightx = 0.0; gbc.weighty = 0.0; gbc.anchor = this.labelAnchor; gbc.fill = GridBagConstraints.HORIZONTAL; listContainer.add(label, gbc); gbc.fill = GridBagConstraints.BOTH; gbc.gridx = gbc.gridx + 1; // FIXME: components default to the labelAnchor - should be different? gbc.weightx = 1.0; gbc.weighty = 0.0; listContainer.add(field, gbc); if (key.equals(HIDDEN)) { // these components are not to be shown setText(label, " "); field.setVisible(false); } fields.add(field); // using list not map to allow for duplicate keys labels.add(label); // ditto // handle adding on-the-fly updateGaps(); this.revalidate(); this.repaint(); } /** * Introspects a component to set the action command and to add the InfoPanel to * its list of ActionListeners. * * @param aComponent The Component to be introspected. * @param aKey The action command to be set. */ protected void introspectComponent(Component aComponent, String aKey) { // try to set properties of whatever component this might be try { Method[] methods = (Method[]) _method_cache.get(aComponent.getClass()); if (methods == null) { Class componentClass = aComponent.getClass(); BeanInfo info = Introspector.getBeanInfo(componentClass); MethodDescriptor[] descriptors = info.getMethodDescriptors(); Method setMethod = null; Method addMethod = null; for (int i = 0; ((setMethod == null || addMethod == null) && i < descriptors.length); i++) { Method m = descriptors[i].getMethod(); String name = m.getName(); if (setMethod == null && name.equals("setActionCommand")) { setMethod = m; } else if (addMethod == null && name.equals("addActionListener")) { addMethod = m; } } methods = new Method[] { setMethod, addMethod }; _method_cache.put(componentClass, methods); } if (methods[0] != null) { methods[0].invoke(aComponent, new Object[] { aKey }); } if (methods[1] != null) { methods[1].invoke(aComponent, new Object[] { this }); listenedToComponents.add(aComponent); } } catch (Exception exc) { // error occured while introspecting... move along. System.out.println("InfoPanel.introspectComponent: " + exc); } } /** * Called to populate a label component with the specified text. This * implementation attempts to call setText(String) on the component. Override to * customize. */ protected void setText(Component c, String text) { try { Method m = c.getClass().getMethod("setText", new Class[] { String.class }); if (m != null) { m.invoke(c, new Object[] { text }); } } catch (Exception exc) { // no such method: ignore } } /** * Creates spacer components on the reserved first grid row for each column of * labels and fields. This allows us to set the margin for those label columns, * and set the preferred width of the field columns. A list containing the field * spacers should be assigned to the fieldSpacers instance variable. */ private void createSpacers() { if (listContainer.getLayout() instanceof GridBagLayout) { // insert spacers for labels column GridBagLayout gridBag = (GridBagLayout) listContainer.getLayout(); GridBagConstraints constraints = new GridBagConstraints(); constraints.gridy = 0; constraints.fill = GridBagConstraints.HORIZONTAL; fieldSpacers = new LinkedList(); Component fieldSpacer; for (int i = 0; i < this.columns; i++) { constraints.gridx = i * 2; listContainer.add(Box.createHorizontalStrut(this.margin), constraints); constraints.gridx = i * 2 + 1; fieldSpacer = Box.createHorizontalStrut(0); fieldSpacers.add(fieldSpacer); listContainer.add(fieldSpacer, constraints); } } } /** * Updates the insets for all components. */ protected void updateGaps() { if (listContainer.getLayout() instanceof GridBagLayout) { GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); Component c = null; GridBagConstraints gbc = null; double totalWeightY = 0.0; int count = listContainer.getComponentCount(); int i; for (i = 0; i < count; i++) { c = listContainer.getComponent(i); gbc = layout.getConstraints(c); totalWeightY += gbc.weighty; if ((gbc.gridx + 1) % (this.columns * 2) == 0) { // if last component in row gbc.insets = new Insets(0, 0, this.vgap, 0); } else { if (gbc.gridx % 2 == 0) { // is a label column - NOTE: uses eleven pixels before component, per l&f // guide gbc.insets = new Insets(0, 0, this.vgap, 11); } else { // is a component column if (gbc.gridy != 0) { if (c instanceof JPanel) ((JPanel) c).setPreferredSize(null); gbc.insets = new Insets(0, 0, this.vgap, this.hgap); } } } layout.setConstraints(c, gbc); } // hack: gridbag clumps components in center if weighty is zero // if sum of weighty is zero, top-justify the list container this.remove(listContainer); if (totalWeightY == 0.0) { this.add(listContainer, BorderLayout.NORTH); } else // put list container in center so it will grow { this.add(listContainer, BorderLayout.CENTER); } } } /** * Updates the label alignment. */ protected void updateLabels() { if (listContainer.getLayout() instanceof GridBagLayout) { GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); Component c = null; GridBagConstraints gbc = null; Iterator it = labels.iterator(); while (it.hasNext()) { c = (Component) it.next(); if (c instanceof JLabel) { ((JLabel) c).setHorizontalAlignment(labelAlign); } gbc = layout.getConstraints(c); gbc.anchor = this.labelAnchor; layout.setConstraints(c, gbc); } } } /** * Convenience method that uses a stock JTextField. * * @param key A string that will be displayed in a label, preferrably unique. * @param value A string that will be displayed in a textfield. */ public void addPair(String key, String value) { addPair(key, value, null); } /** * Convenience method that uses the specified JTextField or subclass and sets it * to the specified value. * * @param key A string that will be displayed in a label, preferrably * unique. * @param value A string that will be displayed in a textfield. * @param textField A JTextField or subclass that will be used to display the * value. If null, a stock JTextField will be used. */ public void addPair(String key, String value, JTextField textField) { if (value == null) { value = ""; } JTextField field = textField; if (field == null) { field = new JTextField(15); // default to 15 columns } else { field = textField; } field.setText(value); addPair(key, (Component) field); } /** * Removes all components from the list. Buttons, if any, will remain unchanged * - use setButtons( null ) to remove them. NOTE: does not call * super.removeAll(). */ public void removeAll() { Object component; Method method; Class[] paramClasses = new Class[] { ActionListener.class }; Object[] paramObjects = new Object[] { this }; Iterator iterator = listenedToComponents.iterator(); while (iterator.hasNext()) { component = iterator.next(); try { method = component.getClass().getMethod("removeActionListener", paramClasses); if (method != null) { method.invoke(component, paramObjects); } } catch (Exception exception) { // No removeActionListener() method, move along. } } listenedToComponents.clear(); listContainer.removeAll(); fields.clear(); labels.clear(); this.revalidate(); this.repaint(); // FIXME: It is very confusing that this // implementation does not call super.removeAll(). } /** * Adds one or buttons to the bottom of the panel with the specified labels from * left to right. Any action listeners will receive action events from clicks on * these buttons - the supplied label will be used as the action command. * * @param buttons A string array containing the strings to be used for the * button labels and action commands. A null value will remove * the button panel. * @see ButtonPanel */ public void setButtons(String[] buttons) { if (buttonPanel == null) { buttonPanel = new ButtonPanel(); buttonPanel.setInsets(new Insets(6, 0, 0, 0)); // button panel has a 11-pixel top inset // and java l&f guide says 17-pixels before command buttons buttonPanel.addActionListener(this); this.add(buttonPanel, BorderLayout.SOUTH); } if (buttons == null) { this.remove(buttonPanel); buttonPanel = null; } else { buttonPanel.setLabels(buttons); } this.revalidate(); this.repaint(); } protected Collection listenedToComponents = new LinkedList(); /** * Retrieves the names of the buttons that are displayed, if any. * * @return A string array containing the strings used for the button labels and * action commands, or null if no buttons have been created. * @see ButtonPanel */ public String[] getButtons() { if (buttonPanel == null) { return null; // none created } return buttonPanel.getLabels(); } /** * Retrieves the actual button panel, if any. * * @return A button panel, or null if none has been created. * @see ButtonPanel */ public ButtonPanel getButtonPanel() { return buttonPanel; } /** * Sets whether the values displayed in the panel should be editable. Defaults * to true. * * @param isEditable Whether the values should be editable. */ public void setEditable(boolean isEditable) { this.isEditable = isEditable; Iterator enumeration = fields.iterator(); while (enumeration.hasNext()) { ((Component) enumeration.next()).setEnabled(isEditable); } } /** * Gets whether the values displayed in the panel are editable. * * @return Whether the values should be editable. */ public boolean isEditable() { return this.isEditable; } /** * Sets the field associated with the key to the specified value. Note: If the * component does not respond to setText() or setString() or setValue() the * value will not be set. JTextFields and the like will work. * * @param key A string representing the key associated with the field. Nulls * are converted to an empty string. * @param value A object to be displayed in the specified field. Nulls are * converted to an empty string. */ public void setValueForKey(String key, Object value) { setValueForKey(key, value, 0); } /** * Sets the field associated with the key to the specified value. Note: If the * component does not respond to setText() or setString() or setValue() the * value will not be set. JTextFields and the like will work. * * @param key A string representing the key associated with the field. Nulls * are converted to an empty string. * @param value A object to be displayed in the specified field. Nulls are * converted to an empty string. */ public void setValueForKey(String key, Object value, int index) { if (key == null) { key = ""; } Container field = null; for (int i = 0; i < fields.size(); i++) { field = (Container) fields.get(i); if (key.equals(field.getName())) { setValueForIndex(index, i, value); return; } } // else not found - ignore } /** * Sets the first field at the specified row index to the specified value. Note: * If the component does not respond to setText() or setString() or setValue() * the value will not be set. JTextFields and the like will work. * * @param row The row index of the component. * @param value A object to be displayed in the specified field. Nulls are * converted to an empty string. */ public void setValueForIndex(int row, Object value) { setValueForIndex(row, 0, value); } /** * Sets the field at the specified row index and column index to the specified * value. Note: If the component does not respond to setText() or setString() or * setValue() the value will not be set. JTextFields and the like will work. * * @param row The row index of the component. * @param index The column index of the component. * @param value A object to be displayed in the specified field. Nulls are * converted to an empty string. */ public void setValueForIndex(int row, int col, Object value) { Container field = (Container) fields.get(row); Component c = field.getComponent(col); setValueForComponent(c, value); } /** * Sets the value in the field at the specified index. Note: If the component * does not respond to setText() or setString() or setValue() this method will * return null. JTextFields and the like will work. * * @param A valid index. * @param value A object to be displayed in the specified field. */ protected void setValueForComponent(Component aComponent, Object value) { // try to set a text or string property try { BeanInfo info = Introspector.getBeanInfo(aComponent.getClass()); MethodDescriptor[] methods = info.getMethodDescriptors(); for (int i = 0; i < methods.length; i++) { Method m = methods[i].getMethod(); Class[] paramTypes = m.getParameterTypes(); if (paramTypes.length == 1) { if (m.getName().equals("setText")) { if (paramTypes[0].getName().equals(String.class.getName())) { m.invoke(aComponent, new Object[] { value }); } } if (m.getName().equals("setString")) { if (paramTypes[0].getName().equals(String.class.getName())) { m.invoke(aComponent, new Object[] { value }); } } if (m.getName().equals("setValue")) { if (paramTypes[0].getName().equals(Object.class.getName())) { m.invoke(aComponent, new Object[] { value }); } } } } } catch (Exception exc) { // error occured while introspecting... move along. // FIXME: should log error in ErrorManager System.out.println("InfoPanel.setValueForComponent: " + exc); } } /** * Gets the value in the field at the specified index. Note: If the component * does not respond to getText() or getString() or getSelectedItem() this method * will return null. JTextFields and the like will work. * * @param A valid index. * @return An object representing the value in the field at the specified index, * or null if the component does not have a text property or if the * index is out of bounds. */ public Object getValueForIndex(int anIndex) { return getValueForIndex(anIndex, 0); } /** * Gets the value in the field at the specified row and column. Note: If the * component does not respond to getText() or getString() or getSelectedItem() * this method will return null. JTextFields and the like will work. * * @param A valid index. * @return An object representing the value in the field at the specified index, * or null if the component does not have a text property or if the * index is out of bounds. */ public Object getValueForIndex(int row, int col) { if ((row >= fields.size()) || (row < 0)) { // out of bounds return null; } Container field = (Container) fields.get(row); Component c = field.getComponent(col); return getValueForComponent(c); } /** * Gets the value in the field associated with the key. Note: If the component * does not respond to getText() or getString() or getSelectedItem() this method * will return null. JTextFields and the like will work. * * @param key An string representing the key associated with the field. Nulls * are converted to an empty string. * @return An object representing the value in the field associated with the * key, or null if the key does not exist or if the component does not * have a text property. */ public Object getValueForKey(String key) { return getValueForKey(key, 0); } /** * Gets the value in the field associated with the key. Note: If the component * does not respond to getText() or getString() or getSelectedItem() this method * will return null. JTextFields and the like will work. * * @param key An string representing the key associated with the field. Nulls * are converted to an empty string. * @return An object representing the value in the field associated with the * key, or null if the key does not exist or if the component does not * have a text property. */ public Object getValueForKey(String key, int index) { if (key == null) { key = ""; } Container field = null; Iterator enumeration = fields.iterator(); while (enumeration.hasNext()) { // finds first value in list with specified key field = (Container) enumeration.next(); if (key.equals(field.getName())) { Component c = field.getComponent(index); if (c != null) { return getValueForComponent(c); } } } // else not found return null; } /** * Gets the value in the specified component. Note: If the component does not * respond to getText() or getString() or getSelectedItem() this method will * return null. JTextFields and the like will work. * * @param aComponent The specified component. * @return An object representing the value in the component. or null if the * component does not have a text property. */ protected Object getValueForComponent(Component aComponent) { // try to get a text or string property try { BeanInfo info = Introspector.getBeanInfo(aComponent.getClass()); MethodDescriptor[] methods = info.getMethodDescriptors(); for (int i = 0; i < methods.length; i++) { Method m = methods[i].getMethod(); Class[] paramTypes = m.getParameterTypes(); if (m.getName().equals("getText")) { if (paramTypes.length == 0) { return m.invoke(aComponent, new Object[] {}); } } if (m.getName().equals("getString")) { if (paramTypes.length == 0) { return m.invoke(aComponent, new Object[] {}); } } if (m.getName().equals("getSelectedItem")) { if (paramTypes.length == 0) { return m.invoke(aComponent, new Object[] {}); } } // TODO: should also handle variants of setValue() } } catch (Exception exc) { // error occured while introspecting... move along. System.out.println("InfoPanel.getValueFromComponent: " + exc); } // not found return null; } /** * Gets the component associated with the key as a JTextField, for backwards * compatibility. * * @param key A string representing the key associated with the component. Nulls * are converted to an empty string. * @return A JTextField that contains the value associated with the key, or null * if the key does not exist or if the component is not a JTextField. */ public JTextField getFieldForKey(String key) { Component c = getComponentForKey(key); if (c instanceof JTextField) { return (JTextField) c; } return null; } /** * Gets the component associated with the key. If more than one component is * associated with the key, returns the first such component. * * @param key A string representing the key associated with the component. Nulls * are converted to an empty string. * @return A component that contains the value associated with the key, or null * if the key does not exist. */ public Component getComponentForKey(String key) { return getComponentForKey(key, 0); } /** * Gets the component associated with the key and index. * * @param key A string representing the key associated with the component. Nulls * are converted to an empty string. * @return A component that contains the value associated with the key, or null * if the key does not exist. */ public Component getComponentForKey(String key, int index) { Container c = getCompositeComponentForKey(key); if (c == null) return null; return c.getComponent(index); } /** * Gets the component at the specified row. If more than one component exists on * that row, returns the first such component. * * @return A component or null if the row does not exist. */ public Object getComponentForIndex(int row) { return getComponentForIndex(row, 0); } /** * Gets the component at the specified row and column. * * @return A component or null if the index is out of bounds. */ public Object getComponentForIndex(int row, int col) { if ((row > fields.size()) || (row < 0)) { // out of bounds return null; } Container field = (Container) fields.get(row); return field.getComponent(col); } /** * Gets the container associated with the key. * * @param key A string representing the key associated with the component. Nulls * are converted to an empty string. * @return A component that contains the value associated with the key, or null * if the key does not exist. */ protected Container getCompositeComponentForKey(String key) { if (key == null) { key = ""; } JPanel field = null; Iterator enumeration = fields.iterator(); while (enumeration.hasNext()) { // finds first value in list with specified key field = (JPanel) enumeration.next(); if (key.equals(field.getName())) { return field; } } // else not found return null; } /** * Provided for backwards compatibility: calls getLabelComponentForKey. * * @param key A string representing the key associated with the compoent. Nulls * are converted to an empty string. * @return Component label object associated with the key, or null if the key * does not exist or if the label component is not an instance of * JLabel. */ public JLabel getLabelForKey(String key) { Component result = getLabelComponentForKey(key); if (result instanceof JLabel) return (JLabel) result; return null; } /** * Get the label component associated with the key. * * @param key A string representing the key associated with the compoent. Nulls * are converted to an empty string. * @return Component label object associated with the key, or null if the key * does not exist. */ public Component getLabelComponentForKey(String key) { if (key == null) { key = ""; } Component label = null; Iterator enumeration = labels.iterator(); while (enumeration.hasNext()) { // finds first value in list with specified key label = (Component) enumeration.next(); if (key.equals(label.getName())) { return label; } } // else not found return null; } /** * Replaces the first component associated with the key. Any value in the * existing component will be copied to the new component. * * @param key A string representing the key to be associated with the component. * Nulls are converted to an empty string. * @param c A component to be placed next to the label corresponding to the * key. Nulls are converted to a JTextField. */ public void setComponentForKey(String key, Component c) { setComponentForKey(key, c, 0); } /** * Replaces the component associated with the key. Any value in the existing * component will be copied to the new component. * * @param key A string representing the key to be associated with the component. * Nulls are converted to an empty string. * @param c A component to be placed next to the label corresponding to the * key. Nulls are converted to a JTextField. */ public void setComponentForKey(String key, Component c, int index) { if (c == null) { c = new JTextField(15); } if (key == null) { key = ""; } Container container = this.getCompositeComponentForKey(key); Component field = container.getComponent(index); Object value = this.getValueForKey(key, index); if (field != null) { container.remove(index); container.add(c, index); c.setEnabled(this.isEditable); introspectComponent(c, key); setValueForComponent(c, value); } } /** * Replaces the first component in the specified row. Any value in the existing * component will be copied to the new component. * * @param row A valid index. * @param c A component to be placed next to the label corresponding to the * key. */ public void setComponentForIndex(int row, Component c) { setComponentForIndex(row, 0, c); } /** * Replaces the component associated with the key. Any value in the existing * component will be copied to the new component. * * @param row A valid index. * @param c A component to be placed next to the label corresponding to the * key. */ public void setComponentForIndex(int row, int col, Component c) { setComponentForKey(getLabels()[row], c, col); } /** * Sets the string that appears before each label's text on the panel. * * @param aString A String to be used as the label prefix. */ public void setLabelPrefix(String aString) { prefix = aString; setLabels(getLabels()); // force refresh } /** * Gets the string that appears before each label's text on the panel. Defaults * to "", an empty string. * * @return A String that is currently used as the label prefix. */ public String getLabelPrefix() { return prefix; } /** * Sets the string that appears after each label's text on the panel. Defaults * to ": ", a colon followed by a space. * * @param aString A String to be used as the label postfix. */ public void setLabelPostfix(String aString) { postfix = aString; setLabels(getLabels()); // force refresh } /** * Gets the string that appears after each label's text on the panel. * * @return A String that is currently used as the label postfix. */ public String getLabelPostfix() { return postfix; } /** * Adds an action listener to the list that will be notified by events occurring * in the panel. * * @param l An action listener to be notified. */ public void addActionListener(ActionListener l) { actionListener = AWTEventMulticaster.add(actionListener, l); } /** * Removes an action listener from the list that will be notified by events * occurring in the panel. * * @param l An action listener to be removed. */ public void removeActionListener(ActionListener l) { actionListener = AWTEventMulticaster.remove(actionListener, l); } /** * Notifies all registered action listeners of a pending Action Event. * * @param e An action event to be broadcast. */ protected void broadcastEvent(ActionEvent e) { if (actionListener != null) { actionListener.actionPerformed(e); } } // interface ActionListener /** * Called by buttons on panel and by other components that might be set to * broadcast events to this listener. Simply forwards the action event * unchanged. * * @param e An action event to be received. */ public void actionPerformed(ActionEvent e) { // if ( e.getSource() instanceof AbstractButton ) // { broadcastEvent(e); // } } /** * GridBagLayout allocates weightx only after considering the preferred width of * the components in a column. We'd prefer that preferred width wasn't * considered, so that the layout worked more like a html-table. GridBagLayout * is poorly factored for subclassing, so this code is going to get a little bit * ugly. Really, what good is a protected method that returns a private class? * Would have liked to just override getLayoutInfo and be done with it. */ private class BetterGridBagLayout extends GridBagLayout { public Dimension preferredLayoutSize(Container parent) { preprocess(); return super.preferredLayoutSize(parent); } public Dimension minimumLayoutSize(Container parent) { preprocess(); return super.minimumLayoutSize(parent); } public void layoutContainer(Container parent) { preprocess(); super.layoutContainer(parent); } protected void preprocess() { if (fieldSpacers == null) return; Iterator i; // find the field with the widest preferred size Component c; int maxWidth = 0; i = fields.iterator(); while (i.hasNext()) { c = (Component) i.next(); maxWidth = Math.max(maxWidth, Math.max(c.getPreferredSize().width, c.getMinimumSize().width)); } // set each column's spacers to that preferred size Dimension min = new Dimension(0, 0); Dimension pref = new Dimension(maxWidth, 0); i = fieldSpacers.iterator(); while (i.hasNext()) { ((Box.Filler) i.next()).changeShape(min, pref, pref); } } } }