/*
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.FlowLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.Vector;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
/**
* ButtonPanel handles display and event broadcasting of standard buttons like
* OK/Cancel/Save/etc. The constructor takes a list or array of strings, each
* representing a button to appear on the panel from left to right. Any button
* click will send an action event to all listeners with the action command
* containing the corresponding string. Note action events are simply forwarded
* from the buttons themselves, so the source of the event will be the button,
* not the button panel. The button panel is the source of the STATE_CHANGED
* events that notify about changes to the panel itself.
*
*
* @author michael@mpowers.net
* @author $Author: cgruber $
* @version $Revision: 904 $
*/
public class ButtonPanel extends JPanel implements ActionListener, MouseMotionListener {
// TODO: Button text should be read from resources.
/**
* Specifies a "OK" button. This is also the action command sent by the OK
* button.
*/
public static final String OK = "OK";
/**
* Specifies a "Save" button. This is also the action command sent by the Save
* button.
*/
public static final String SAVE = "Save";
/**
* Specifies a "Refresh" button. This is also the action command sent by the
* Refresh button.
*/
public static final String REFRESH = "Refresh";
/**
* Specifies a "Clear All" button. This is also the action command sent by the
* Clear All button.
*/
public static final String CLEAR_ALL = "Clear All";
/**
* Specifies a "Refresh" button. This is also the action command sent by the
* Cancel button.
*/
public static final String CANCEL = "Cancel";
/**
* Specifies a "Yes" button. This is also the action command sent by the Yes
* button.
*/
public static final String YES = "Yes";
/**
* Specifies a "No" button. This is also the action command sent by the No
* button.
*/
public static final String NO = "No";
/**
* Specifies an "Add" button. This is also the action command sent by the Add
* button.
*/
public static final String ADD = "Add";
/**
* Specifies a "Remove" button. This is also the action command sent by the
* Remove button.
*/
public static final String REMOVE = "Remove";
/**
* This is the action command to all listeners when the button state is changed.
*/
public static final String STATE_CHANGED = "STATE_CHANGED";
/**
* This is the container to which buttons are added.
*/
protected Container buttonContainer = null; // useful for subclasses
/**
* This is the list of all buttons on the panel.
*/
protected Vector buttonList = null;
/**
* The insets for this panel, so they can be modified.
*/
protected Insets insets = new Insets(5, 5, 5, 5);
/**
* This is the layout manager - which must be a FlowLayout or subclass.
*/
protected FlowLayout buttonPanelLayout = null;
// for action multicasting
protected ActionListener actionListener = null;
/**
* Constructs a ButtonPanel. Three buttons are created so the panel is filled
* when used in a GUI-builder environment.
*/
public ButtonPanel() {
buttonList = new Vector();
initLayout();
// default labels for bean layout
setLabels(new String[] { "One", "Two", "Three" });
}
/**
* This method is responsible for the initial layout of the panel. Subclasses
* can implement different layouts, but this method is responsible for
* initializing buttonContainer and buttonPanelLayout and setting the container
* to use the layout.
*/
protected void initLayout() {
this.setInsets(super.getInsets());
buttonContainer = this;
buttonPanelLayout = new BetterFlowLayout(BetterFlowLayout.RIGHT);
buttonContainer.setLayout(buttonPanelLayout);
((BetterFlowLayout) buttonPanelLayout).setWidthUniform(true);
// setBackground( Color.blue ); // useful for debugging
}
/**
* Constructs a ButtonPanel using specified buttons.
*
* @param buttonList An array containing the strings to be used in labeling the
* buttons.
*/
public ButtonPanel(String[] buttonList) {
this();
setLabels(buttonList);
}
/**
* Constructs a ButtonPane using specified actions. For each action, a button is
* created, that when pressed the corresponding action is activated. The "name"
* of the action is used as the title of the button.
*
* @param actionList An array of actions to be used to create buttons with.
*/
public ButtonPanel(Action[] actionList) {
this();
setLabels(actionList);
}
/**
* Creates the buttons to appear on the panel. Any existing buttons are
* replaced. The labels are used as names and action commands in addition to
* labels.
*
* @param labels An array of strings to be used in labeling the buttons. If
* null, all buttons will be removed.
*/
public void setLabels(String[] labels) {
if (labels == null) {
labels = new String[] {};
}
buttonContainer.removeAll();
this.buttonList = new Vector(labels.length);
String item = null;
Component button;
for (int i = 0; i < labels.length; i++) {
item = labels[i];
if (item != null) {
button = createComponentWithLabel(item.toString());
this.buttonList.addElement(item);
addComponentToPanel(button);
button.setEnabled(this.isEnabled());
/*
* if ( i == 0 ) { JRootPane root = SwingUtilities.getRootPane( button ); if (
* root != null ) root.setDefaultButton( button ); }
*/
} else {
throw new IllegalArgumentException("ButtonPanel.setButtons: nulls are not allowed.");
}
}
this.revalidate();
this.repaint();
broadcastEvent(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, STATE_CHANGED));
}
/**
*
*/
public void setLabels(Action[] actions) {
if (actions == null) {
actions = new Action[] {};
}
buttonContainer.removeAll();
this.buttonList = new Vector(actions.length);
Action action = null;
Component button;
for (int i = 0; i < actions.length; i++) {
action = actions[i];
if (action != null) {
String name = (String) action.getValue(Action.NAME);
button = createComponentWithLabel(name);
this.buttonList.addElement(name);
addComponentToPanel(button);
button.setEnabled(this.isEnabled() ? action.isEnabled() : false);
// Add the action to the "button" if it knows about action listeners.
try {
Method addActionListenerMethod = button.getClass().getMethod("addActionListener",
new Class[] { ActionListener.class });
addActionListenerMethod.invoke(button, new Object[] { action });
} catch (NoSuchMethodException e) {
/* Do Nothing */ } catch (IllegalAccessException e) {
e.printStackTrace();
/* TODO: Do Something? */ } catch (InvocationTargetException e) {
e.printStackTrace();
/* TODO: Do Something? */ }
// Create a new listener for property change events and have
// the action broadcast to that listener.
PropertyChangeListener pcListener = new ActionChangeListener(button);
action.addPropertyChangeListener(pcListener);
} else {
throw new IllegalArgumentException("ButtonPanel.setButtons: nulls are not allowed.");
}
}
this.revalidate();
this.repaint();
broadcastEvent(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, STATE_CHANGED));
}
/**
* Gets the labels of the buttons that appear on the panel, ordered from left to
* right.
*
* @return A new list containing strings used in labeling the buttons.
*/
public String[] getLabels() {
String[] labels = new String[buttonList.size()];
int i = 0;
for (Enumeration it = buttonList.elements(); it.hasMoreElements();) {
labels[i++] = it.nextElement().toString();
}
return labels;
}
/**
* Gets the first component having the specified name.
*
* @return A component with the specified name, or null if none match.
*/
public Component getButton(String aLabel) {
if (aLabel == null)
return null;
Component c = null;
int count = buttonContainer.getComponentCount();
for (int i = 0; i < count; i++) {
c = buttonContainer.getComponent(i);
if (aLabel.equals(c.getName())) {
return c;
}
}
return null;
}
/**
* Creates a new component with the specified label. The label is also used for
* the component's name and action command, if any. (This implementation returns
* a JButton.)
*
* @param aLabel The label for the component that will be created.
* @return The newly created component.
*/
protected Component createComponentWithLabel(String aLabel) {
String buttonLabel = aLabel; // TODO: get string from resource
JButton newButton = new JButton(); // might allow other types in future
newButton.setName(aLabel);
newButton.setText(buttonLabel);
newButton.setActionCommand(aLabel);
newButton.addActionListener(this);
return newButton;
}
/**
* Adds a component to the right-most side of the layout.
*
* @param aComponent The component to be added to the layout.
*/
protected void addComponentToPanel(Component aComponent) {
buttonContainer.add(aComponent);
}
/**
* Changes the alignment of the buttons in the panel. Defaults to
* right-justified.
*
* @param alignment A valid alignment code, per BetterFlowLayout implementation.
* @see BetterFlowLayout
*/
public void setAlignment(int alignment) {
buttonPanelLayout.setAlignment(alignment);
buttonContainer.doLayout();
}
/**
* Gets the alignment of the buttons in the panel.
*
* @return An alignment code, per FlowLayout implementation.
* @see FlowLayout
*/
public int getAlignment() {
return buttonPanelLayout.getAlignment();
}
/**
* Changes the horizontal spacing between components in the panel.
*
* @param newHgap the new spacing, in pixels. May not be negative.
*/
public void setHgap(int newHgap) {
if (newHgap < 0)
return; // may not be negative
buttonPanelLayout.setHgap(newHgap);
}
/**
* Gets the current horizontal spacing between components.
*
* @return the current horizontal spacing, in pixels.
*/
public int getHgap() {
return buttonPanelLayout.getHgap();
}
/**
* Changes the vertical spacing between components in the panel.
*
* @param newVgap the new spacing, in pixels. May not be negative.
*/
public void setVgap(int newVgap) {
if (newVgap < 0)
return; // may not be negative
buttonPanelLayout.setVgap(newVgap);
}
/**
* Gets the current vertical spacing between components.
*
* @return the current vertical spacing, in pixels.
*/
public int getVgap() {
return buttonPanelLayout.getVgap();
}
/**
* Changes the insets for this panel.
*
* @param newInsets the new insets.
*/
public void setInsets(Insets newInsets) {
insets = newInsets;
}
/**
* Overridden to return the user-specified insets for this panel.
*
* @return the current insets for this panel.
*/
public Insets getInsets() {
return insets;
}
/**
* Overridden to call setEnabled on all components on panel.
*
* @param isEnabled whether to enable the panel and all components on it.
*/
public void setEnabled(boolean isEnabled) {
super.setEnabled(isEnabled);
int count = buttonContainer.getComponentCount();
for (int i = 0; i < count; i++) {
buttonContainer.getComponent(i).setEnabled(isEnabled);
}
}
// Action Multicast methods
/**
* Adds an action listener to the list that will be notified by button events
* and changes in button state.
*
* @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 button
* events and changes in button state.
*
* @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.
*
* @param e An action event to be received.
*/
public void actionPerformed(ActionEvent e) {
broadcastEvent(e);
}
/**
* A property change listener that listens specifically for property changes
* from action objects. This is the class that ties in the action to the button.
* This class is added to an action as a property change listener. The
* corresponding component is referenced by this class toe easily handle updates
* to the component caused by changes to the action.
*/
public class ActionChangeListener implements PropertyChangeListener {
/** The UI component that is affected by the action's changes. */
Component theComponent;
/**
* Constructs an ActionChangeListener with the given component being the
* recipient of the action's changes.
*
* @param The component to bind with the action.
*/
public ActionChangeListener(Component aComponent) {
super();
theComponent = aComponent;
}
/**
* Called whenever a property changes on the action object.
*
* @pram e The property change event generated by the action.
*/
public void propertyChange(PropertyChangeEvent e) {
String propertyName = e.getPropertyName();
if (propertyName.equals(Action.NAME)) {
String name = (String) e.getNewValue();
if (theComponent instanceof AbstractButton) {
AbstractButton button = (AbstractButton) theComponent;
String oldName = button.getName();
button.setText(name);
button.setName(name);
button.setActionCommand(name);
// Replace the old name of the component with the new name
// in the ButtonPanel's list of components.
buttonList.setElementAt(name, buttonList.indexOf(oldName));
}
// TODO: If component is not a button (or doesn't define the getText()
// then what should be done.
} else if (propertyName.equals("enabled")) {
Boolean enabled = (Boolean) e.getNewValue();
theComponent.setEnabled(ButtonPanel.this.isEnabled() ? enabled.booleanValue() : false);
}
// TODO: Icon?
}
}
// for testing
public static void main(String[] argv) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception exc) {
}
JFrame dialog = new JFrame();
BorderLayout bl = new BorderLayout(20, 20);
ButtonPanel panel = new ButtonPanel();
// ButtonPanel panel = new ButtonPanel( new String[] { "OkayOkay", "CancelCancel" } );
dialog.getContentPane().setLayout(bl);
dialog.getContentPane().add(panel, BorderLayout.CENTER);
dialog.setLocation(50, 50);
// dialog.setSize( 450, 150 );
panel.setAlignment(BetterFlowLayout.CENTER_VERTICAL);
panel.getButton("One").setEnabled(false);
dialog.pack();
dialog.setVisible(true);
try {
BeanInfo info = Introspector.getBeanInfo(ButtonPanel.class);
PropertyDescriptor[] props = info.getPropertyDescriptors();
for (int i = 0; i < props.length; i++) {
System.out.println(props[i].getName());
}
} catch (Exception exc) {
System.out.println(exc);
}
}
public void mouseDragged(MouseEvent e) {
}
public void mouseMoved(MouseEvent e) {
}
}