/*
* AP(r) Computer Science GridWorld Case Study:
* Copyright(c) 2005-2006 Cay S. Horstmann (http://horstmann.com)
*
* This code is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation.
*
* This code 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 General Public License for more details.
*
* @author Cay Horstmann
*/
package info.gridworld.gui;
import info.gridworld.grid.Grid;
import info.gridworld.grid.Location;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.beans.PropertyEditorSupport;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.ResourceBundle;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
* Makes the menus for constructing new occupants and grids, and for invoking
* methods on existing occupants. <br />
* This code is not tested on the AP CS A and AB exams. It contains GUI
* implementation details that are not intended to be understood by AP CS
* students.
*/
public class MenuMaker<T>
{
/**
* Constructs a menu maker for a given world.
* @param parent the frame in which the world is displayed
* @param resources the resource bundle
* @param displayMap the display map
*/
public MenuMaker(WorldFrame<T> parent, ResourceBundle resources,
DisplayMap displayMap)
{
this.parent = parent;
this.resources = resources;
this.displayMap = displayMap;
}
/**
* Makes a menu that displays all public methods of an object
* @param occupant the object whose methods should be displayed
* @param loc the location of the occupant
* @return the menu to pop up
*/
public JPopupMenu makeMethodMenu(T occupant, Location loc)
{
this.occupant = occupant;
this.currentLocation = loc;
JPopupMenu menu = new JPopupMenu();
Method[] methods = getMethods();
Class oldDcl = null;
for (int i = 0; i < methods.length; i++)
{
Class dcl = methods[i].getDeclaringClass();
if (dcl != Object.class)
{
if (i > 0 && dcl != oldDcl)
menu.addSeparator();
menu.add(new MethodItem(methods[i]));
}
oldDcl = dcl;
}
return menu;
}
/**
* Makes a menu that displays all public constructors of a collection of
* classes.
* @param classes the classes whose constructors should be displayed
* @param loc the location of the occupant to be constructed
* @return the menu to pop up
*/
public JPopupMenu makeConstructorMenu(Collection<Class> classes,
Location loc)
{
this.currentLocation = loc;
JPopupMenu menu = new JPopupMenu();
boolean first = true;
Iterator<Class> iter = classes.iterator();
while (iter.hasNext())
{
if (first)
first = false;
else
menu.addSeparator();
Class cl = iter.next();
Constructor[] cons = (Constructor[]) cl.getConstructors();
for (int i = 0; i < cons.length; i++)
{
menu.add(new OccupantConstructorItem(cons[i]));
}
}
return menu;
}
/**
* Adds menu items that call all public constructors of a collection of
* classes to a menu
* @param menu the menu to which the items should be added
* @param classes the collection of classes
*/
public void addConstructors(JMenu menu, Collection<Class> classes)
{
boolean first = true;
Iterator<Class> iter = classes.iterator();
while (iter.hasNext())
{
if (first)
first = false;
else
menu.addSeparator();
Class cl = iter.next();
Constructor[] cons = cl.getConstructors();
for (int i = 0; i < cons.length; i++)
{
menu.add(new GridConstructorItem(cons[i]));
}
}
}
private Method[] getMethods()
{
Class cl = occupant.getClass();
Method[] methods = cl.getMethods();
Arrays.sort(methods, new Comparator<Method>()
{
public int compare(Method m1, Method m2)
{
int d1 = depth(m1.getDeclaringClass());
int d2 = depth(m2.getDeclaringClass());
if (d1 != d2)
return d2 - d1;
int d = m1.getName().compareTo(m2.getName());
if (d != 0)
return d;
d1 = m1.getParameterTypes().length;
d2 = m2.getParameterTypes().length;
return d1 - d2;
}
private int depth(Class cl)
{
if (cl == null)
return 0;
else
return 1 + depth(cl.getSuperclass());
}
});
return methods;
}
/**
* A menu item that shows a method or constructor.
*/
private class MCItem extends JMenuItem
{
public String getDisplayString(Class retType, String name,
Class[] paramTypes)
{
StringBuffer b = new StringBuffer();
b.append("<html>");
if (retType != null)
appendTypeName(b, retType.getName());
b.append(" <font color='blue'>");
appendTypeName(b, name);
b.append("</font>( ");
for (int i = 0; i < paramTypes.length; i++)
{
if (i > 0)
b.append(", ");
appendTypeName(b, paramTypes[i].getName());
}
b.append(" )</html>");
return b.toString();
}
public void appendTypeName(StringBuffer b, String name)
{
int i = name.lastIndexOf('.');
if (i >= 0)
{
String prefix = name.substring(0, i + 1);
if (!prefix.equals("java.lang"))
{
b.append("<font color='gray'>");
b.append(prefix);
b.append("</font>");
}
b.append(name.substring(i + 1));
}
else
b.append(name);
}
public Object makeDefaultValue(Class type)
{
if (type == int.class)
return new Integer(0);
else if (type == boolean.class)
return Boolean.FALSE;
else if (type == double.class)
return new Double(0);
else if (type == String.class)
return "";
else if (type == Color.class)
return Color.BLACK;
else if (type == Location.class)
return currentLocation;
else if (Grid.class.isAssignableFrom(type))
return currentGrid;
else
{
try
{
return type.newInstance();
}
catch (Exception ex)
{
return null;
}
}
}
}
private abstract class ConstructorItem extends MCItem
{
public ConstructorItem(Constructor c)
{
setText(getDisplayString(null, c.getDeclaringClass().getName(), c
.getParameterTypes()));
this.c = c;
}
public Object invokeConstructor()
{
Class[] types = c.getParameterTypes();
Object[] values = new Object[types.length];
for (int i = 0; i < types.length; i++)
{
values[i] = makeDefaultValue(types[i]);
}
if (types.length > 0)
{
PropertySheet sheet = new PropertySheet(types, values);
JOptionPane.showMessageDialog(this, sheet, resources
.getString("dialog.method.params"),
JOptionPane.QUESTION_MESSAGE);
values = sheet.getValues();
}
try
{
return c.newInstance(values);
}
catch (InvocationTargetException ex)
{
parent.new GUIExceptionHandler().handle(ex.getCause());
return null;
}
catch (Exception ex)
{
parent.new GUIExceptionHandler().handle(ex);
return null;
}
}
private Constructor c;
}
private class OccupantConstructorItem extends ConstructorItem implements
ActionListener
{
public OccupantConstructorItem(Constructor c)
{
super(c);
addActionListener(this);
setIcon(displayMap.getIcon(c.getDeclaringClass(), 16, 16));
}
@SuppressWarnings("unchecked")
public void actionPerformed(ActionEvent event)
{
T result = (T) invokeConstructor();
parent.getWorld().add(currentLocation, result);
parent.repaint();
}
}
private class GridConstructorItem extends ConstructorItem implements
ActionListener
{
public GridConstructorItem(Constructor c)
{
super(c);
addActionListener(this);
setIcon(displayMap.getIcon(c.getDeclaringClass(), 16, 16));
}
@SuppressWarnings("unchecked")
public void actionPerformed(ActionEvent event)
{
Grid<T> newGrid = (Grid<T>) invokeConstructor();
parent.setGrid(newGrid);
}
}
private class MethodItem extends MCItem implements ActionListener
{
public MethodItem(Method m)
{
setText(getDisplayString(m.getReturnType(), m.getName(), m
.getParameterTypes()));
this.m = m;
addActionListener(this);
setIcon(displayMap.getIcon(m.getDeclaringClass(), 16, 16));
}
public void actionPerformed(ActionEvent event)
{
Class[] types = m.getParameterTypes();
Object[] values = new Object[types.length];
for (int i = 0; i < types.length; i++)
{
values[i] = makeDefaultValue(types[i]);
}
if (types.length > 0)
{
PropertySheet sheet = new PropertySheet(types, values);
JOptionPane.showMessageDialog(this, sheet, resources
.getString("dialog.method.params"),
JOptionPane.QUESTION_MESSAGE);
values = sheet.getValues();
}
try
{
Object result = m.invoke(occupant, values);
parent.repaint();
if (m.getReturnType() != void.class)
{
String resultString = result.toString();
Object resultObject;
final int MAX_LENGTH = 50;
final int MAX_HEIGHT = 10;
if (resultString.length() < MAX_LENGTH)
resultObject = resultString;
else
{
int rows = Math.min(MAX_HEIGHT, 1
+ resultString.length() / MAX_LENGTH);
JTextArea pane = new JTextArea(rows, MAX_LENGTH);
pane.setText(resultString);
pane.setLineWrap(true);
resultObject = new JScrollPane(pane);
}
JOptionPane.showMessageDialog(parent, resultObject,
resources.getString("dialog.method.return"),
JOptionPane.INFORMATION_MESSAGE);
}
}
catch (InvocationTargetException ex)
{
parent.new GUIExceptionHandler().handle(ex.getCause());
}
catch (Exception ex)
{
parent.new GUIExceptionHandler().handle(ex);
}
}
private Method m;
}
private T occupant;
private Grid currentGrid;
private Location currentLocation;
private WorldFrame<T> parent;
private DisplayMap displayMap;
private ResourceBundle resources;
}
class PropertySheet extends JPanel
{
/**
* Constructs a property sheet that shows the editable properties of a given
* object.
* @param object the object whose properties are being edited
*/
public PropertySheet(Class[] types, Object[] values)
{
this.values = values;
editors = new PropertyEditor[types.length];
setLayout(new FormLayout());
for (int i = 0; i < values.length; i++)
{
JLabel label = new JLabel(types[i].getName());
add(label);
if (Grid.class.isAssignableFrom(types[i]))
{
label.setEnabled(false);
add(new JPanel());
}
else
{
editors[i] = getEditor(types[i]);
if (editors[i] != null)
{
editors[i].setValue(values[i]);
add(getEditorComponent(editors[i]));
}
else
add(new JLabel("?"));
}
}
}
/**
* Gets the property editor for a given property, and wires it so that it
* updates the given object.
* @param bean the object whose properties are being edited
* @param descriptor the descriptor of the property to be edited
* @return a property editor that edits the property with the given
* descriptor and updates the given object
*/
public PropertyEditor getEditor(Class type)
{
PropertyEditor editor;
editor = defaultEditors.get(type);
if (editor != null)
return editor;
editor = PropertyEditorManager.findEditor(type);
return editor;
}
/**
* Wraps a property editor into a component.
* @param editor the editor to wrap
* @return a button (if there is a custom editor), combo box (if the editor
* has tags), or text field (otherwise)
*/
public Component getEditorComponent(final PropertyEditor editor)
{
String[] tags = editor.getTags();
String text = editor.getAsText();
if (editor.supportsCustomEditor())
{
return editor.getCustomEditor();
}
else if (tags != null)
{
// make a combo box that shows all tags
final JComboBox comboBox = new JComboBox(tags);
comboBox.setSelectedItem(text);
comboBox.addItemListener(new ItemListener()
{
public void itemStateChanged(ItemEvent event)
{
if (event.getStateChange() == ItemEvent.SELECTED)
editor.setAsText((String) comboBox.getSelectedItem());
}
});
return comboBox;
}
else
{
final JTextField textField = new JTextField(text, 10);
textField.getDocument().addDocumentListener(new DocumentListener()
{
public void insertUpdate(DocumentEvent e)
{
try
{
editor.setAsText(textField.getText());
}
catch (IllegalArgumentException exception)
{
}
}
public void removeUpdate(DocumentEvent e)
{
try
{
editor.setAsText(textField.getText());
}
catch (IllegalArgumentException exception)
{
}
}
public void changedUpdate(DocumentEvent e)
{
}
});
return textField;
}
}
public Object[] getValues()
{
for (int i = 0; i < editors.length; i++)
if (editors[i] != null)
values[i] = editors[i].getValue();
return values;
}
private PropertyEditor[] editors;
private Object[] values;
private static Map<Class, PropertyEditor> defaultEditors;
// workaround for Web Start bug
public static class StringEditor extends PropertyEditorSupport
{
public String getAsText()
{
return (String) getValue();
}
public void setAsText(String s)
{
setValue(s);
}
}
static
{
defaultEditors = new HashMap<Class, PropertyEditor>();
defaultEditors.put(String.class, new StringEditor());
defaultEditors.put(Location.class, new LocationEditor());
defaultEditors.put(Color.class, new ColorEditor());
}
}