/*
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);
}
}
}
}