// Copyright (c) 2006 - 2008, Markus Strauch.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
package net.sf.sdedit.ui.components.configuration;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.border.TitledBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import net.sf.sdedit.ui.components.ButtonPanel;
/**
* A component for configuring the properties of a {@linkplain Bean}.
*
* @author Markus Strauch
*
* @param <C>
* the type of the data object to be configured
*/
@SuppressWarnings("serial")
public class ConfigurationUI<C extends DataObject> extends JPanel {
private Map<String, JPanel> categoryMap;
private ConfiguratorFactory<C> configuratorFactory;
private Map<String, List<Configurator<?, C>>> configurators;
private Map<String, Integer> labelWidths;
private JList categoryList;
private JPanel right;
private ConfigurationDialog parent;
private Bean<C> bean;
private Bean<C> formerState;
private Bean<C> defaultBean;
private ButtonPanel buttonPanel;
private JPanel categoryListPanel;
/**
* Constructor.
*
* @param parent
* the dialog that contains this configuration component
* @param bean
* the bean to be configured
* @param defaultBean
* a bean with values that can be restored, or null
* @param saveAsDefault
* a string for the button that is associated to the action that
* saves the bean's values as the defaultBean's values,
* optionally followed by '|' and a tool-tip, or null, if no such
* button should be visible
* @param loadDefault
* a string for the button that is associated to the action that
* restores the bean's values from the defaultBean's values,
* optionally followed by '|' and a tool-tip, or null, if no such
* button should be visible
* @param description
* a description to appear at the top of this configuration
* component
*/
public ConfigurationUI(ConfigurationDialog parent, Bean<C> bean,
Bean<C> defaultBean, String saveAsDefault, String loadDefault,
String description) {
super();
this.bean = bean;
formerState = bean.copy();
this.defaultBean = defaultBean;
setLayout(new BorderLayout());
this.parent = parent;
right = new JPanel();
right.setLayout(new BorderLayout());
right.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));
configuratorFactory = new ConfiguratorFactory<C>();
configurators = new HashMap<String, List<Configurator<?, C>>>();
categoryListPanel = new JPanel();
categoryListPanel.setLayout(new BorderLayout());
categoryListPanel.add(new JLabel("Categories:"), BorderLayout.NORTH);
categoryList = new JList();
JScrollPane listScrollPane = new JScrollPane(categoryList);
categoryListPanel.add(listScrollPane, BorderLayout.CENTER);
add(categoryListPanel, BorderLayout.WEST);
add(right, BorderLayout.CENTER);
if (description != null) {
JLabel descriptionLabel = new JLabel(description);
descriptionLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5,
5));
add(descriptionLabel, BorderLayout.NORTH);
}
labelWidths = new HashMap<String, Integer>();
categoryMap = new TreeMap<String, JPanel>();
categoryList.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
String category = (String) categoryList.getSelectedValue();
if (category != null) {
JPanel panel = categoryMap.get(category);
right.removeAll();
right.add(panel, BorderLayout.CENTER);
right.updateUI();
}
}
});
final ListCellRenderer lcr = categoryList.getCellRenderer();
categoryList.setCellRenderer(new ListCellRenderer() {
public Component getListCellRendererComponent(JList list,
Object value, int index, boolean isSelected,
boolean cellHasFocus) {
JComponent comp = (JComponent) lcr
.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus);
comp.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
return comp;
}
});
buttonPanel = new ButtonPanel();
add(buttonPanel, BorderLayout.SOUTH);
buttonPanel.addAction(cancel);
if (loadDefault != null) {
String[] split = loadDefault.split("\\|");
String name = split[0];
String tooltip = split.length == 1 ? "" : split[1];
buttonPanel.addAction(restoreDefaultsAction(name, tooltip));
}
if (saveAsDefault != null) {
String[] split = saveAsDefault.split("\\|");
String name = split[0];
String tooltip = split.length == 1 ? "" : split[1];
buttonPanel.addAction(saveAsDefaultAction(name, tooltip));
}
buttonPanel.addAction(ok);
init(bean, defaultBean);
refreshAll();
}
public void hideCategoryList() {
remove(categoryListPanel);
}
public void hideButtons() {
remove(buttonPanel);
}
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
categoryList.setEnabled(enabled);
for (Configurator<?, C> configurator : getConfigurators()) {
configurator.setEnabled(enabled
&& configurator.isDependencySatisfied());
}
}
private List<Configurator<?, C>> getConfigurators() {
List<Configurator<?, C>> list = new LinkedList<Configurator<?, C>>();
for (List<Configurator<?, C>> sublist : configurators.values()) {
list.addAll(sublist);
}
return list;
}
/**
* Returns for each property category of the bean the panel where the
* properties can be configured
*
* @return the panels for the bean's property categories
*/
public Collection<JPanel> getCategoryPanels() {
return categoryMap.values();
}
private void init(Bean<C> bean, Bean<C> defaultObject) {
for (PropertyDescriptor property : bean.getProperties()) {
if (property.getWriteMethod().getAnnotation(Adjustable.class)
.editable()) {
add(bean, property, defaultObject);
}
}
for (Map.Entry<String, List<Configurator<?,C>>> entry : configurators.entrySet()) {
int width = labelWidths.get(entry.getKey());
for (Configurator<?,C> configurator : entry.getValue()) {
configurator.setLabelWidth(width);
}
}
}
@SuppressWarnings("unchecked")
private void add(Bean<C> bean, PropertyDescriptor property,
Bean<C> defaultObject) {
Method writeMethod = property.getWriteMethod();
if (writeMethod.isAnnotationPresent(Adjustable.class)) {
Adjustable adj = writeMethod.getAnnotation(Adjustable.class);
if (adj.editable()) {
String category = adj.category();
JPanel categoryPanel = categoryMap.get(category);
int labelWidth = 0;
if (categoryPanel == null) {
categoryPanel = new JPanel();
categoryPanel.setLayout(new BoxLayout(categoryPanel,
BoxLayout.Y_AXIS));
categoryPanel.setBorder(new TitledBorder(category));
configurators.put(category,
new LinkedList<Configurator<?, C>>());
categoryMap.put(category, categoryPanel);
categoryList.setListData(categoryMap.keySet().toArray());
if (categoryMap.keySet().iterator().next().equals(category)) {
right.removeAll();
right.add(categoryPanel, BorderLayout.CENTER);
}
categoryList.setSelectedIndex(0);
} else {
labelWidth = labelWidths.get(category);
}
Configurator<?, C> configurator = configuratorFactory
.createConfigurator(bean, property, defaultObject);
if (adj.tooltip().length() > 0) {
configurator.setToolTipText(adj.tooltip());
}
labelWidth = Math.max(labelWidth, configurator.getLabelWidth());
labelWidths.put(category, labelWidth);
int height = configurator.getPreferredSize().height;
Dimension size = new Dimension(500, height);
configurator.setMinimumSize(size);
configurator.setPreferredSize(size);
configurator.setMaximumSize(size);
configurator.setAlignmentX(0F);
configurators.get(category).add(configurator);
categoryPanel.add(configurator);
int gap = 15 * adj.gap();
categoryPanel
.add(Box.createRigidArea(new Dimension(1, gap)));
}
} else {
throw new IllegalArgumentException(
"No Adjustable annotation present for property "
+ property.getName());
}
}
public void setBean(Bean<C> bean) {
this.bean = bean;
this.formerState = bean.copy();
for (Configurator<?, C> configurator : getConfigurators()) {
configurator.setBean(bean);
}
}
private Action ok = new AbstractAction() {
{
putValue(Action.NAME, "OK");
putValue(Action.SHORT_DESCRIPTION,
"Acknowledges all changes made since this dialog has been opened");
}
public void actionPerformed(ActionEvent e) {
parent.setVisible(false);
parent.apply();
}
};
/**
* Calls {@linkplain Configurator#refresh()} on all configurators that
* belong to this configuration component, so that they will reflect the
* values of their corresponding properties thereafter.
*/
public void refreshAll() {
for (Configurator<?, C> configurator : getConfigurators()) {
configurator.refresh();
}
}
public void apply() {
formerState.takeValuesFrom(bean);
}
public void cancel() {
bean.takeValuesFrom(formerState);
}
private Action cancel = new AbstractAction() {
{
putValue(Action.NAME, "Cancel");
putValue(Action.SHORT_DESCRIPTION,
"Cancels all changes made since this dialog has been opened");
}
public void actionPerformed(ActionEvent e) {
parent.cancel();
parent.setVisible(false);
}
};
private Action restoreDefaultsAction(final String name, final String tooltip) {
return new AbstractAction() {
{
putValue(Action.NAME, name);
putValue(Action.SHORT_DESCRIPTION, tooltip);
}
public void actionPerformed(ActionEvent e) {
if (defaultBean != null) {
bean.takeValuesFrom(defaultBean);
}
}
};
}
private Action saveAsDefaultAction(final String name, final String tooltip) {
return new AbstractAction() {
{
putValue(Action.NAME, name);
putValue(Action.SHORT_DESCRIPTION, tooltip);
}
public void actionPerformed(ActionEvent e) {
if (defaultBean != null) {
defaultBean.takeValuesFrom(bean);
}
}
};
}
}