/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* 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.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* 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.
*/
/*
* JavaGemGenerator.java
* Creation date: Oct 8, 2003
* By: Frank Worsley
*/
package org.openquark.gems.client.generators;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.font.TextAttribute;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.LayoutFocusTraversalPolicy;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.border.Border;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.openquark.cal.compiler.LanguageInfo;
import org.openquark.cal.compiler.ModuleTypeInfo;
import org.openquark.cal.compiler.QualifiedName;
import org.openquark.cal.compiler.Scope;
import org.openquark.cal.compiler.TypeChecker;
import org.openquark.cal.compiler.TypeConstructor;
import org.openquark.cal.compiler.UnableToResolveForeignEntityException;
import org.openquark.cal.compiler.SourceModel.ModuleDefn;
import org.openquark.cal.module.Cal.Core.CAL_Prelude;
import org.openquark.cal.services.Perspective;
import org.openquark.gems.client.GemCutter;
import org.openquark.gems.client.ValueRunner;
import org.openquark.gems.client.utilities.MouseClickDragAdapter;
import org.openquark.gems.client.valueentry.ValueEditorManager;
/**
* This is the container class for the Java gem factories to import Java types.
* @author Frank Worsley
*/
public final class JavaGemGenerator {
/** The icon used by the generator. */
private static final Icon GENERATOR_ICON = new ImageIcon(GemGenerator.class.getResource("/Resources/supercombinator.gif"));
private JavaGemGenerator() {}
/**
* This is the Java generator that imports Java methods, constructors and fields as foreign functions.
* @author Frank Worsley
*/
public static class JavaFunctionGenerator implements GemGenerator {
/**
* @see org.openquark.gems.client.generators.GemGenerator#launchGenerator(javax.swing.JFrame, org.openquark.cal.services.Perspective, org.openquark.gems.client.ValueRunner, org.openquark.gems.client.valueentry.ValueEditorManager, org.openquark.cal.compiler.TypeChecker)
*/
public GemGenerator.GeneratedDefinitions launchGenerator(JFrame parent,
Perspective perspective,
ValueRunner valueRunner,
ValueEditorManager valueEditorManager,
TypeChecker typeChecker) {
if (parent == null || perspective == null) {
throw new NullPointerException();
}
final JavaGemGeneratorDialog generatorUI = new JavaGemGeneratorDialog(parent, perspective, false);
generatorUI.setVisible(true);
return new GemGenerator.GeneratedDefinitions() {
public ModuleDefn getModuleDefn() {
return null;
}
public Map<String, String> getSourceElementMap() {
return generatorUI.getSourceDefinitions();
}
};
}
/**
* @see org.openquark.gems.client.generators.GemGenerator#getGeneratorMenuName()
*/
public String getGeneratorMenuName() {
return GeneratorMessages.getString("JGF_MethodImportMenuName");
}
/**
* @see org.openquark.gems.client.generators.GemGenerator#getGeneratorTitle()
*/
public String getGeneratorTitle() {
return GeneratorMessages.getString("JGF_MethodImportTitle");
}
/**
* @see org.openquark.gems.client.generators.GemGenerator#getGeneratorIcon()
*/
public Icon getGeneratorIcon() {
return GENERATOR_ICON;
}
}
/**
* This is the Java generator that imports Java classes as foreign data types.
* @author Frank Worsley
*/
public static class JavaDataTypeGenerator implements GemGenerator {
/**
* @see org.openquark.gems.client.generators.GemGenerator#launchGenerator(javax.swing.JFrame, org.openquark.cal.services.Perspective, org.openquark.gems.client.ValueRunner, org.openquark.gems.client.valueentry.ValueEditorManager, org.openquark.cal.compiler.TypeChecker)
*/
public GemGenerator.GeneratedDefinitions launchGenerator(JFrame parent,
Perspective perspective,
ValueRunner valueRunner,
ValueEditorManager valueEditorManager,
TypeChecker typeChecker) {
if (parent == null || perspective == null) {
throw new NullPointerException();
}
final JavaGemGeneratorDialog generatorUI = new JavaGemGeneratorDialog(parent, perspective, true);
generatorUI.setVisible(true);
return new GemGenerator.GeneratedDefinitions() {
public ModuleDefn getModuleDefn() {
return null;
}
public Map<String, String> getSourceElementMap() {
return generatorUI.getSourceDefinitions();
}
};
}
/**
* @see org.openquark.gems.client.generators.GemGenerator#getGeneratorMenuName()
*/
public String getGeneratorMenuName() {
return GeneratorMessages.getString("JGF_DataTypeImportName");
}
/**
* @see org.openquark.gems.client.generators.GemGenerator#getGeneratorTitle()
*/
public String getGeneratorTitle() {
return GeneratorMessages.getString("JGF_MethodImportTitle");
}
/**
* @see org.openquark.gems.client.generators.GemGenerator#getGeneratorIcon()
*/
public Icon getGeneratorIcon() {
return GENERATOR_ICON;
}
}
}
/**
* This is the user interface class for either of the Java factories.
* @author Frank Worsley
*/
class JavaGemGeneratorDialog extends JDialog {
private static final long serialVersionUID = 7978474516115445555L;
/** The icon to use for error messages. */
private static final Icon ERROR_ICON = new ImageIcon(GemCutter.class.getResource("/Resources/error.gif"));
/** The icon to use for warning messages. */
private static final Icon WARNING_ICON = new ImageIcon(GemCutter.class.getResource("/Resources/warning.gif"));
/** The icon to use if everything is ok. */
private static final Icon OK_ICON = new ImageIcon(GemCutter.class.getResource("/Resources/checkmark.gif"));
/** The perspective this UI is running in. */
private final Perspective perspective;
/**
* Map from source name to source code.
* The list of source definitions we want to create. Ordered by insertion order, so that
* definitions we want to create first will be created first.
*/
private final Map<String, String> sourceDefinitions = new LinkedHashMap<String, String>();
/**
* Map of Java class to new CAL data declaration name
* that we need to add to import the foreign function.
*/
private final Map<Class<?>, QualifiedName> newDataTypes = new HashMap<Class<?>, QualifiedName>();
/** A specialized JList for displaying the members of a Java class. */
private final JavaMemberList memberList = new JavaMemberList();
/** The text field for entering the name of the new gem. */
private final JTextField gemNameField = new JTextField();
/** The text field for entering the comment for the new gem. */
private final JTextField commentField = new JTextField();
/** The text field for entering the class name. */
private final JComboBox classNameCombo = new JComboBox();
/** The radio button for selecting private scope. */
private final JRadioButton privateButton = new JRadioButton(GeneratorMessages.getString("PrivateLabel"));
/** The radio button for selecting public scope. */
private final JRadioButton publicButton = new JRadioButton(GeneratorMessages.getString("PublicLabel"));
/** The button group for the radio buttons. */
private final ButtonGroup buttonGroup = new ButtonGroup();
/** The label for displaying status messages. */
private final JLabel statusLabel = new JLabel();
/** Whether or not we are adding a data type. */
private final boolean addingDataType;
/** The OK button for the dialog. */
private JButton okButton = null;
/** The cancel button for the dialog. */
private JButton cancelButton = null;
/** Whether or not the user has typed into the name field. */
private boolean userHasTyped = false;
/** The last class name we used to populate the combo. */
private String lastClassName = null;
/**
* Constructor for a new generator ui.
* @param parent the parent of the dialog
* @param perspective the perspective the UI should use
* @param addingDataType whether to display the UI for adding a data type
*/
public JavaGemGeneratorDialog(JFrame parent, Perspective perspective, boolean addingDataType) {
super(parent, true);
if (perspective == null) {
throw new NullPointerException();
}
this.perspective = perspective;
this.addingDataType = addingDataType;
String titleId = addingDataType ? "JGF_DataTypeImportTitle" : "JGF_MethodImportTitle";
setTitle(GeneratorMessages.getString(titleId));
getContentPane().setLayout(new BorderLayout());
getContentPane().add(getTitlePanel(), BorderLayout.NORTH);
getContentPane().add(getMainPanel(), BorderLayout.CENTER);
getContentPane().add(getButtonPanel(), BorderLayout.SOUTH);
getRootPane().setDefaultButton(getOkButton());
buttonGroup.add(publicButton);
buttonGroup.add(privateButton);
buttonGroup.setSelected(publicButton.getModel(), true);
memberList.setOnlyShowConstructors(addingDataType);
updateState();
// Make the class name field have default focus
setFocusTraversalPolicy(new LayoutFocusTraversalPolicy() {
private static final long serialVersionUID = 3866828721074107359L;
public Component getDefaultComponent(Container c) {
return classNameCombo;
}
});
// Add listeners to update the error message if things change
gemNameField.addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent e) {
userHasTyped = true;
updateState();
}
});
memberList.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
updateState();
}
});
// Let the user double click on the list to select an import
memberList.addMouseListener(new MouseClickDragAdapter() {
public boolean mouseReallyClicked(MouseEvent e) {
boolean doubleClicked = super.mouseReallyClicked(e);
if (doubleClicked && SwingUtilities.isLeftMouseButton(e) && okButton.isEnabled()) {
okButton.doClick();
}
return doubleClicked;
}
});
KeyListener dismissKeyListener = new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
dispose();
}
}
};
// Cancel the dialog if the user presses ESC
addKeyListener(dismissKeyListener);
okButton.addKeyListener(dismissKeyListener);
cancelButton.addKeyListener(dismissKeyListener);
privateButton.addKeyListener(dismissKeyListener);
publicButton.addKeyListener(dismissKeyListener);
gemNameField.addKeyListener(dismissKeyListener);
commentField.addKeyListener(dismissKeyListener);
memberList.addKeyListener(dismissKeyListener);
pack();
setResizable(false);
setSize(500, getSize().height);
// position in the center of the parent window
int x = parent.getX() + parent.getWidth() / 2 - getWidth() / 2;
int y = parent.getY() + parent.getHeight() / 2 - getHeight() / 2;
setLocation(Math.max(parent.getX(), x), Math.max(parent.getY(), y));
}
/**
* @return the new source definitions that should be created
*/
public Map<String, String> getSourceDefinitions() {
return sourceDefinitions;
}
/**
* Updates the state of the Ok button to only be enabled if the user has entered
* all required information. Also updates the information message displayed.
*/
private void updateState() {
// Make sure that you check for errors first, then for warnings.
// Be careful about the order of checks, more important checks come first.
// Note that the gem name check is more important than the class name check.
// This is so the default gem name is set to nothing if an invalid class name is entered.
if (!checkForValidGemName()) {
return;
}
// check if a valid class name is entered
if (memberList.getCurrentClass() == null) {
String message = null;
if (addingDataType) {
message = GeneratorMessages.getString("JGF_InvalidClassNameForType");
} else {
message = GeneratorMessages.getString("JGF_InvalidClassName");
}
statusLabel.setText(message);
statusLabel.setToolTipText(message);
statusLabel.setIcon(ERROR_ICON);
okButton.setEnabled(false);
return;
}
// check if a class member is selected
Object selectedValue = memberList.getSelectedValue();
if (selectedValue == null && !addingDataType) {
String message = GeneratorMessages.getString("JGF_NoMemberSelected");
statusLabel.setText(message);
statusLabel.setToolTipText(message);
statusLabel.setIcon(ERROR_ICON);
okButton.setEnabled(false);
return;
}
// check if a gem or data type with the given name already exists
String gemName = gemNameField.getText();
if (addingDataType && perspective.getWorkingModuleTypeInfo().getTypeConstructor(gemName) != null) {
String message = GeneratorMessages.getString("JGF_DataTypeExists");
statusLabel.setText(message);
statusLabel.setToolTipText(message);
statusLabel.setIcon(ERROR_ICON);
okButton.setEnabled(false);
return;
} else if (!addingDataType && perspective.getWorkingModuleTypeInfo().getFunction(gemName) != null) {
String message = GeneratorMessages.getString("JGF_GemExists");
statusLabel.setText(message);
statusLabel.setToolTipText(message);
statusLabel.setIcon(WARNING_ICON);
okButton.setEnabled(true);
return;
}
// check if the selected data type is already imported under a different name
QualifiedName existingName;
try {
existingName = getExistingTypeName(memberList.getCurrentClass());
} catch (UnableToResolveForeignEntityException e) {
String message = GeneratorMessages.getString("JGF_UnableToResolveForeignEntity", e.getCompilerMessage().getMessage());
statusLabel.setText(message);
statusLabel.setToolTipText(message);
statusLabel.setIcon(ERROR_ICON);
okButton.setEnabled(false);
return;
}
if (addingDataType && existingName != null) {
String message = GeneratorMessages.getString("JGF_DataTypeAlreadyImported", existingName.getQualifiedName());
statusLabel.setText(message);
statusLabel.setToolTipText(message);
statusLabel.setIcon(WARNING_ICON);
okButton.setEnabled(true);
return;
}
// check if the user has selected a constructor
if (addingDataType && selectedValue == null) {
String message = null;
if (memberList.getModel().getSize() == 0) {
message = GeneratorMessages.getString("JGF_NoConstructors");
} else {
message = GeneratorMessages.getString("JGF_NoConstructorSelected");
}
statusLabel.setText(message);
statusLabel.setToolTipText(message);
statusLabel.setIcon(WARNING_ICON);
okButton.setEnabled(true);
return;
}
// everything is fine, so enable the button
String message = GeneratorMessages.getString(addingDataType ? "JGF_OkImportType" : "JGF_OkImportMember");
statusLabel.setText(message);
statusLabel.setToolTipText(message);
statusLabel.setIcon(OK_ICON);
okButton.setEnabled(true);
}
/**
* Checks the vailidity of the name in the gem name field and suggests a default
* name is no name has been entered.
* For data types the suggested name is the class name with a 'J' prepended.
* For methods/fields the suggested name is the method/field name with a 'j' prepended.
* For constructors the suggested name is the class name with 'jMake' prepended.
* @return true if the provided name is valid, false otherwise
*/
private boolean checkForValidGemName() {
String gemName = gemNameField.getText();
if (!userHasTyped || (gemName.length() == 0 && !gemNameField.isFocusOwner())) {
// Provide a default name for the user if he hasn't typed a name himself.
String newName = null;
if (addingDataType && memberList.getCurrentClass() != null) {
Class<?> currentClass = memberList.getCurrentClass();
newName = getUniqueTypeName(currentClass).getUnqualifiedName();
} else if (!addingDataType && memberList.getCurrentClass() != null && memberList.getSelectedValue() != null) {
// Get the name of the class the member belongs to.
String className = getUniqueTypeName(memberList.getCurrentClass()).getUnqualifiedName();
Object value = memberList.getSelectedValue();
if (value instanceof Constructor<?>) {
newName = "make" + className;
} else if (value instanceof Method) {
newName = "j" + className.substring(1) + "_" + ((Method) value).getName();
} else if (value instanceof Field) {
newName = "j" + className.substring(1) + "_" + ((Field) value).getName();
} else {
throw new IllegalStateException("invalid item in the member list");
}
}
// Update the field with the suggested name
gemNameField.setText(newName);
} else {
// If the user has typed a name, check if for validity.
String messageId = null;
if (addingDataType && !LanguageInfo.isValidTypeConstructorName(gemName)) {
messageId = "JGF_InvalidDataTypeName";
} else if (!addingDataType && !LanguageInfo.isValidFunctionName(gemName)) {
messageId = "JGF_InvalidGemName";
}
if (messageId != null) {
statusLabel.setText(GeneratorMessages.getString(messageId));
statusLabel.setIcon(ERROR_ICON);
okButton.setEnabled(false);
return false;
}
}
return true;
}
/**
* @return the white title panel that appears at the top of the dialog
*/
private JPanel getTitlePanel() {
JPanel titlePanel = new JPanel();
titlePanel.setBackground(Color.WHITE);
Border compoundBorder = BorderFactory.createCompoundBorder(BorderFactory.createEtchedBorder(),
BorderFactory.createEmptyBorder(5, 5, 5, 5));
titlePanel.setBorder(compoundBorder);
titlePanel.setLayout(new BorderLayout(5, 5));
String titleId = addingDataType ? "JGF_DataTypeImportTitle" : "JGF_MethodImportTitle";
JLabel titleLabel = new JLabel(GeneratorMessages.getString(titleId));
titleLabel.setFont(getFont().deriveFont(Font.BOLD, getFont().getSize() + 2));
titlePanel.add(titleLabel, BorderLayout.NORTH);
String subTitleId = addingDataType ? "JGF_DataTypeImportSubTitle" : "JGF_MethodImportSubTitle";
JLabel subTitleLabel = new JLabel(GeneratorMessages.getString(subTitleId));
titlePanel.add(subTitleLabel, BorderLayout.SOUTH);
return titlePanel;
}
/**
* @return the main panel that shows the contents of the dialog
*/
private JPanel getMainPanel() {
JPanel javaPanel = new JPanel();
javaPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
javaPanel.setLayout(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.NORTHWEST;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 1;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = new Insets(5, 5, 10, 5);
javaPanel.add(statusLabel, constraints);
statusLabel.setFont(getFont().deriveFont(Font.BOLD));
constraints.gridx = 1;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.gridwidth = 1;
javaPanel.add(new JLabel(GeneratorMessages.getString("JGF_ClassNameHeader")), constraints);
constraints.gridx = 2;
constraints.weightx = 1;
constraints.weighty = 0;
constraints.gridwidth = GridBagConstraints.REMAINDER;
javaPanel.add(getClassNameCombo(), constraints);
constraints.gridx = 1;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.gridwidth = 1;
javaPanel.add(new JLabel(GeneratorMessages.getString("JGF_MembersHeader")), constraints);
JScrollPane listScrollPane = new JScrollPane(memberList);
listScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
listScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
constraints.gridx = 2;
constraints.weightx = 1;
constraints.weighty = 1;
constraints.fill = GridBagConstraints.BOTH;
constraints.gridwidth = GridBagConstraints.REMAINDER;
javaPanel.add(listScrollPane, constraints);
constraints.gridx = 1;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.gridwidth = 1;
constraints.insets = new Insets(5, 5, 5, 5);
javaPanel.add(new JLabel(GeneratorMessages.getString(addingDataType ? "JGF_TypeNameHeader" : "JGF_GemNameHeader")), constraints);
constraints.gridx = 2;
constraints.weightx = 1;
constraints.weighty = 0;
constraints.gridwidth = GridBagConstraints.REMAINDER;
gemNameField.setColumns(20);
javaPanel.add(gemNameField, constraints);
constraints.gridx = 1;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.gridwidth = 1;
javaPanel.add(new JLabel(GeneratorMessages.getString("JGF_CommentHeader")), constraints);
constraints.gridx = 2;
constraints.weightx = 1;
constraints.weighty = 0;
constraints.gridwidth = GridBagConstraints.REMAINDER;
gemNameField.setColumns(20);
javaPanel.add(commentField, constraints);
constraints.gridx = 1;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.gridwidth = 1;
javaPanel.add(new JLabel(GeneratorMessages.getString("JGF_VisibilityHeader")), constraints);
constraints.gridx = 2;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.gridwidth = 1;
javaPanel.add(publicButton, constraints);
constraints.gridx = 3;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.gridwidth = 1;
javaPanel.add(privateButton, constraints);
constraints.gridx = 4;
constraints.weightx = 1;
constraints.weighty = 0;
constraints.gridwidth = GridBagConstraints.REMAINDER;
javaPanel.add(new JLabel(""), constraints);
return javaPanel;
}
/**
* @return the panel that contains the buttons at the bottom of the dialog
*/
private JPanel getButtonPanel() {
JPanel buttonPanel = new JPanel();
buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
buttonPanel.add(Box.createHorizontalGlue());
buttonPanel.add(getOkButton());
buttonPanel.add(Box.createHorizontalStrut(5));
buttonPanel.add(getCancelButton());
return buttonPanel;
}
/**
* @return the OK button for the dialog
*/
private JButton getOkButton() {
if (okButton == null) {
Action okAction = new AbstractAction(GeneratorMessages.getString("JGF_OkButton")) {
private static final long serialVersionUID = 8780497621064785865L;
public void actionPerformed(ActionEvent e) {
try {
generateSource();
} catch (UnableToResolveForeignEntityException ex) {
JOptionPane.showMessageDialog(
JavaGemGeneratorDialog.this,
GeneratorMessages.getString("JGF_UnableToResolveForeignEntity", ex.getCompilerMessage().getMessage()),
GeneratorMessages.getString("JGF_ErrorDialogTitle"),
JOptionPane.ERROR_MESSAGE);
}
dispose();
}
};
okButton = new JButton(okAction);
okButton.setPreferredSize(getCancelButton().getPreferredSize());
}
return okButton;
}
/**
* @return the cancel button for the dialog
*/
private JButton getCancelButton() {
if (cancelButton == null) {
Action cancelAction = new AbstractAction(GeneratorMessages.getString("JGF_CancelButton")) {
private static final long serialVersionUID = 1344989545249460016L;
public void actionPerformed(ActionEvent e) {
dispose();
}
};
cancelButton = new JButton(cancelAction);
}
return cancelButton;
}
/**
* @return the text field for entering the Java class name.
*/
private JComboBox getClassNameCombo() {
final Timer classComboTimer = new Timer(300, new ActionListener() {
public void actionPerformed(ActionEvent e) {
updateClassNameCombo();
updateState();
}
});
classComboTimer.setRepeats(false);
classNameCombo.setEditable(true);
classNameCombo.setModel(new DefaultComboBoxModel());
classNameCombo.getEditor().getEditorComponent().addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent e) {
int keyCode = e.getKeyCode();
if (e.getKeyChar() != KeyEvent.CHAR_UNDEFINED &&
keyCode != KeyEvent.VK_ENTER &&
keyCode != KeyEvent.VK_ESCAPE) {
// If the user types a key, restart the timer to update the list.
classComboTimer.restart();
} else if (keyCode == KeyEvent.VK_ENTER && classNameCombo.getSelectedIndex() == -1) {
// If the user hits enter but no item is selected in the combo, then
// select the first item for the user.
if (classNameCombo.getModel().getSize() > 0) {
classNameCombo.setSelectedIndex(0);
}
classNameCombo.hidePopup();
classComboTimer.restart();
e.consume();
}
}
});
// If the list selection changes, restart the timer to update the member list.
classNameCombo.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
classComboTimer.restart();
}
});
return classNameCombo;
}
private void updateClassNameCombo() {
JTextField editor = (JTextField) classNameCombo.getEditor().getEditorComponent();
String className = editor.getText();
String unqualifiedName = className;
// Arrays can be denoted as [Ljava.lang.String; for String[] as an example.
// If the user is entering an array, it doesn't make sense to use the unqualified name.
if (!className.startsWith("[")) {
String[] tokens = className.split("\\.");
unqualifiedName = tokens.length > 0 ? tokens[tokens.length - 1] : className;
}
Class<?>[] classes = resolveClassName(unqualifiedName);
if (!className.equals(lastClassName)) {
// If we're searching for a new class name, then update the combo list.
lastClassName = className;
int newSelected = -1;
boolean shouldShowPopup = classNameCombo.isPopupVisible();
int selectionStart = editor.getSelectionStart();
int selectionEnd = editor.getSelectionEnd();
int caretPosition = editor.getCaretPosition();
DefaultComboBoxModel model = (DefaultComboBoxModel) classNameCombo.getModel();
model.removeAllElements();
if (classes.length == 0) {
editor.setText(className);
editor.setCaretPosition(caretPosition);
editor.setSelectionStart(selectionStart);
editor.setSelectionEnd(selectionEnd);
memberList.updateForClass(null);
return;
}
for (int i = 0; i < classes.length; i++) {
model.addElement(classes[i].getName());
if (classes[i].getName().equals(className)) {
newSelected = i;
}
}
if (newSelected == -1) {
shouldShowPopup = classes.length > 0;
}
// Have to hide and reshow the popup, so that it validates correctly.
classNameCombo.setPopupVisible(false);
classNameCombo.setPopupVisible(shouldShowPopup);
classNameCombo.setSelectedIndex(newSelected);
if (newSelected == -1) {
editor.setText(className);
editor.setCaretPosition(caretPosition);
editor.setSelectionStart(selectionStart);
editor.setSelectionEnd(selectionEnd);
}
}
// Now update the member list for the selected item.
if (classes.length == 0) {
memberList.updateForClass(null);
} else {
int index = classNameCombo.getSelectedIndex();
Class<?> selectedClass = index != -1 ? classes[index] : null;
memberList.updateForClass(selectedClass);
if (selectedClass != null) {
editor.setText(selectedClass.getName());
} else {
editor.setText(className);
}
}
}
/**
* Resolves an unqualified or array class name by returning the classes it resolves to,
* or an empty array if the name cannot be resolved to a class.
* @param className the *unqualified* class name or array class name to resolve
* @return the classes it resolves to or an empty array if no classes can be resolved
*/
private Class<?>[] resolveClassName(String className) {
if (className.startsWith("[")) {
// If we're looking for an array, then the name is automatically fully qualified.
try {
return new Class<?>[] { Class.forName(className) };
} catch (ClassNotFoundException ex) {
// That's ok, class was not found.
return new Class<?>[0];
} catch (Exception ex) {
ex.printStackTrace();
return new Class<?>[0];
}
}
// Looks like we're looking for an unqualified class name.
// Iterate over each package and try to complete the name.
List<Class<?>> classes = new ArrayList<Class<?>>();
Package[] packages = Package.getPackages();
for (final Package aPackage : packages) {
String fullClassName = aPackage.getName() + "." + className;
try {
Class<?> classForName = Class.forName(fullClassName);
if (Modifier.isPublic(classForName.getModifiers())) {
classes.add(classForName);
}
} catch (ClassNotFoundException ex) {
// That's ok, class was not found.
} catch (NoClassDefFoundError ex) {
// This seems to be thrown when the class does not exist, but a class does exist with different case.
// eg. type in "Gemcutter" (the name of the class is "GemCutter").
// TODO: It would be nice to suggest a correction in this situation.
} catch (Exception ex) {
ex.printStackTrace();
}
}
return classes.toArray(new Class<?>[0]);
}
/**
* Generates the source definitions for the Java import that the user has selected.
*/
private void generateSource() throws UnableToResolveForeignEntityException {
String gemSource = null;
String gemName = gemNameField.getText();
String gemComment = commentField.getText();
Scope gemScope = publicButton.getModel().isSelected() ? Scope.PUBLIC : Scope.PRIVATE;
Object javaImport = memberList.getSelectedValue();
if (addingDataType) {
// If we're adding a data type, put it into the map of new types to be added.
newDataTypes.put(memberList.getCurrentClass(), QualifiedName.make(perspective.getWorkingModuleName(), gemName));
// Now pick a sensible name for the constructor that was selected, if any.
gemName = "make" + gemName;
}
// Generate the source for the method/field/constructor declaration.
// This will cause additional needed data types to be added to the new types map.
if (javaImport instanceof Method) {
gemSource = getFunctionDeclaration(gemName, gemComment, gemScope, memberList.getCurrentClass(), (Method) javaImport);
} else if (javaImport instanceof Constructor<?>) {
gemSource = getConstructorDeclaration(gemName, gemComment, gemScope, (Constructor<?>) javaImport);
} else if (javaImport instanceof Field) {
gemSource = getFieldDeclaration(gemName, gemComment, gemScope, memberList.getCurrentClass(), (Field) javaImport);
}
// Add the required data types.
for (final Class<?> dataClass : newDataTypes.keySet()) {
QualifiedName dataName = newDataTypes.get(dataClass);
String dataSource = getDataDeclaration(dataClass, dataName.getUnqualifiedName());
sourceDefinitions.put(dataName.getUnqualifiedName(), dataSource);
}
// Finally add the method/field/constructor definition.
if (javaImport != null) {
sourceDefinitions.put(gemName, gemSource);
}
}
/**
* Generates the source for a data declaration for the given class and name.
* @param javaClass the Java class the data declaration is for
* @param unqualifiedName the unqualified name of the data type to create
* @return the source code
*/
private String getDataDeclaration(Class<?> javaClass, String unqualifiedName) {
StringBuilder source = new StringBuilder(GeneratorMessages.getString("JGF_ForeignDataDeclComment"));
source.append("data foreign unsafe import jvm \"");
source.append(javaClass.getName());
source.append("\" ");
source.append("public ");
source.append(unqualifiedName);
source.append(";\n");
return source.toString();
}
/**
* Generates a new field declaration.
* @param gemName the name of the declaration
* @param gemComment the comment to put in the source
* @param gemScope the scope of the declaration
* @param javaClass the Java class that declares the instance of the field that's called
* @param javaField the java field to import
* @return the source code
*/
private String getFieldDeclaration(final String gemName, final String gemComment, final Scope gemScope, final Class<?> javaClass, final Field javaField) throws UnableToResolveForeignEntityException {
// If a field is not static, the class CAL type has to precede the argument list.
final boolean isStatic = Modifier.isStatic(javaField.getModifiers());
final Class<?>[] javaArgTypes;
// If not static then prepend the class types
if (!isStatic) {
javaArgTypes = new Class<?>[2];
javaArgTypes[0] = javaClass;
javaArgTypes[1] = javaField.getType();
} else {
javaArgTypes = new Class<?>[1];
javaArgTypes[0] = javaField.getType();
}
// Convert the java types to CAL types.
final QualifiedName[] calArgTypes = mapToCALTypes(javaArgTypes);
// Generate source...
final StringBuilder source = new StringBuilder(GeneratorMessages.getString("JGF_ForeignFieldDeclComment"));
if (gemComment != null && gemComment.trim().length() > 0) {
source.append("// " + gemComment + "\n");
}
source.append("foreign unsafe import jvm \"");
if (isStatic) {
source.append("static ");
}
source.append("field ");
if (isStatic) {
source.append(javaClass.getName());
source.append(".");
}
source.append(javaField.getName());
source.append("\" ");
source.append(gemScope.toString()).append(' ');
source.append(gemName);
source.append(" :: ");
for (final QualifiedName calArgType : calArgTypes) {
source.append(calArgType.getQualifiedName());
source.append(" -> ");
}
// Remove trailing arrow
source.delete(source.length() - 4, source.length());
source.append(";\n");
return source.toString();
}
/**
* Generate a new constructor declaration.
* @param gemName the name of the declaration
* @param gemComment the comment to put in the source
* @param gemScope the scope of the declaration
* @param javaConstructor the constructor to import
* @return the source code
*/
private String getConstructorDeclaration(String gemName, String gemComment, Scope gemScope, Constructor<?> javaConstructor) throws UnableToResolveForeignEntityException {
// Determine the java argument types
Class<?>[] consArgTypes = javaConstructor.getParameterTypes();
Class<?>[] javaArgTypes = new Class<?>[consArgTypes.length + 1];
System.arraycopy(consArgTypes, 0, javaArgTypes, 0, consArgTypes.length);
javaArgTypes[javaArgTypes.length - 1] = javaConstructor.getDeclaringClass();
// Convert the java types to CAL types.
QualifiedName[] calArgTypes = mapToCALTypes(javaArgTypes);
StringBuilder source = new StringBuilder(GeneratorMessages.getString("JGF_ForeignConstructorDeclComment"));
if (gemComment != null && gemComment.trim().length() > 0) {
source.append("// " + gemComment + "\n");
}
source.append("foreign unsafe import jvm \"constructor ");
source.append(javaConstructor.getDeclaringClass().getName());
source.append("\" ");
source.append(gemScope.toString()).append(' ');
source.append(gemName);
source.append(" :: ");
for (final QualifiedName calArgType : calArgTypes) {
source.append(calArgType.getQualifiedName());
source.append(" -> ");
}
// Remove trailing arrow
source.delete(source.length() - 4, source.length());
source.append(";\n");
return source.toString();
}
/**
* Generates a new function declaration.
* @param gemName the name of the declaration
* @param gemComment the comment to put in the source
* @param gemScope the scope of the declaration
* @param javaClass the Java class that declares the instance of the method that's called
* @param javaMethod the Java method to import
* @return the source code
*/
private String getFunctionDeclaration(final String gemName, final String gemComment, final Scope gemScope, final Class<?> javaClass, final Method javaMethod) throws UnableToResolveForeignEntityException {
// If a method is not static, the class CAL type has to precede the argument list.
final boolean isStatic = Modifier.isStatic(javaMethod.getModifiers());
final int staticOffset = isStatic ? 0 : 1;
final Class<?>[] methodArgTypes = javaMethod.getParameterTypes();
final Class<?>[] javaArgTypes = new Class<?>[methodArgTypes.length + 1 + staticOffset];
// If not static then prepend the class types
if (!isStatic) {
javaArgTypes[0] = javaClass;
}
// Inset the method argument types.
System.arraycopy(methodArgTypes, 0, javaArgTypes, staticOffset, methodArgTypes.length);
// Add the return type at the end
javaArgTypes[javaArgTypes.length - 1] = javaMethod.getReturnType();
// Convert the java types to CAL types.
final QualifiedName[] calArgTypes = mapToCALTypes(javaArgTypes);
// Generate source...
final StringBuilder source = new StringBuilder(GeneratorMessages.getString("JGF_ForeignFunctionDeclComment"));
if (gemComment != null && gemComment.trim().length() > 0) {
source.append("// " + gemComment + "\n");
}
source.append("foreign unsafe import jvm \"");
if (isStatic) {
source.append("static ");
}
source.append("method ");
if (isStatic) {
source.append(javaClass.getName());
source.append(".");
}
source.append(javaMethod.getName());
source.append("\" ");
source.append(gemScope.toString()).append(' ');
source.append(gemName);
source.append(" :: ");
for (final QualifiedName calArgType : calArgTypes) {
source.append(calArgType.getQualifiedName());
source.append(" -> ");
}
// Remove trailing arrow
source.delete(source.length() - 4, source.length());
source.append(";\n");
return source.toString();
}
/**
* Maps the given Java Class to the QualifiedName of the matching CAL type.
* If there is no matching CAL type a new type name will be created on the fly and an
* entry for it added to the newDataTypes map. These new data types will need to have
* data declaration created for them.
* @param javaType the java type to find a CAL type for
* @param visibleTypes the visible types to search through
* @return the QualifiedName of the matching CAL type
*/
private QualifiedName mapToCALType(Class<?> javaType, TypeConstructor[] visibleTypes) throws UnableToResolveForeignEntityException {
if (javaType == null || visibleTypes == null) {
throw new NullPointerException();
}
// Check to see if we have previously added our own declaration for this type.
if (newDataTypes.containsKey(javaType)) {
return newDataTypes.get(javaType);
}
// If we didn't yet add our own declaration, see if there is an existing one.
// Start with the primitive types, then check the other types.
if (javaType == Character.TYPE) {
return CAL_Prelude.TypeConstructors.Char;
} else if (javaType == Boolean.TYPE) {
return CAL_Prelude.TypeConstructors.Boolean;
} else if (javaType == Byte.TYPE) {
return CAL_Prelude.TypeConstructors.Byte;
} else if (javaType == Short.TYPE) {
return CAL_Prelude.TypeConstructors.Short;
} else if (javaType == Integer.TYPE) {
return CAL_Prelude.TypeConstructors.Int;
} else if (javaType == Long.TYPE) {
return CAL_Prelude.TypeConstructors.Long;
} else if (javaType == Float.TYPE) {
return CAL_Prelude.TypeConstructors.Float;
} else if (javaType == Double.TYPE) {
return CAL_Prelude.TypeConstructors.Double;
} else if (javaType == Void.TYPE) {
return CAL_Prelude.TypeConstructors.Unit;
} else if (javaType == String.class) {
return CAL_Prelude.TypeConstructors.String;
} else {
for (final TypeConstructor visibleType : visibleTypes) {
if (visibleType.getForeignTypeInfo() != null &&
javaType == visibleType.getForeignTypeInfo().getForeignType()) {
return visibleType.getName();
}
}
}
// Looks like we have to make our own data declaration for this.
// Pick a sensible name and add it to the map before returning it.
QualifiedName typeName = getUniqueTypeName(javaType);
newDataTypes.put(javaType, typeName);
return typeName;
}
/**
* Tries to find a unique name for the Java type. It stars the with base class name and
* then prepends package names if that name is ambiguous. This is done so that, for example,
* "java.util.Timer" and "javax.swing.Timer" don't both get imported as "JTimer".
* Instead they might be imported as "JTimer" and "JSwingTimer".
* @param javaType the java type to find a unique name for
* @return a unqiue name for the Java type
*/
private QualifiedName getUniqueTypeName(Class<?> javaType) {
String arrayNameSuffix = "";
while (javaType.isArray()) {
javaType = javaType.getComponentType();
arrayNameSuffix += "Array";
}
String javaTypeName = javaType.getName();
// Arrays may contain primitive types that do not start with a capital
javaTypeName = javaTypeName.substring(0, 1).toUpperCase() + javaTypeName.substring(1);
// Get the fragments of the name.
// The fragments are the package name pieces and the unqualified class name.
String[] nameFragments = javaTypeName.split("\\.");
// By default the type name is simply "J" plus the unqualified class name.
String typeName = "J" + nameFragments[nameFragments.length - 1] + arrayNameSuffix;
ModuleTypeInfo workingModuleTypeInfo = perspective.getWorkingModuleTypeInfo();
boolean outOfFragments = false;
int fragmentIndex = 2;
while (workingModuleTypeInfo.getTypeConstructor(typeName) != null) {
// Disambiguate the name by adding additional fragments.
if (fragmentIndex > nameFragments.length) {
outOfFragments = true;
break;
}
String newFragment = nameFragments[nameFragments.length - fragmentIndex];
newFragment = newFragment.substring(0, 1).toUpperCase() + newFragment.substring(1);
typeName = "J" + newFragment + typeName.substring(1);
fragmentIndex++;
}
if (outOfFragments) {
// If we're out of fragments, then disambiguate by adding number suffixes.
int i = 1;
String basicTypeName = typeName;
while (workingModuleTypeInfo.getTypeConstructor(typeName) != null) {
typeName = basicTypeName + "_" + i;
i++;
}
}
return QualifiedName.make(perspective.getWorkingModuleName(), typeName);
}
/**
* Checks if the given type is already imported and if it is returns
* the matching CAL type name. Otherwise this returns null.
* @param javaType the java type to check for
* @return the CAL type name if the type is imported, null otherwise
*/
private QualifiedName getExistingTypeName(Class<?> javaType) throws UnableToResolveForeignEntityException {
if (javaType == null) {
return null;
}
TypeConstructor[] visibleTypes = perspective.getTypeConstructors();
for (final TypeConstructor visibleType : visibleTypes) {
if (visibleType.getForeignTypeInfo() != null &&
javaType == visibleType.getForeignTypeInfo().getForeignType()) {
return visibleType.getName();
}
}
return null;
}
/**
* Maps all Java types to their matching CAL types.
* @param javaTypes the java types to map
* @return array of QualifiedNames for the matching CAL types
*/
private QualifiedName[] mapToCALTypes(Class<?>[] javaTypes) throws UnableToResolveForeignEntityException {
QualifiedName[] calTypes = new QualifiedName[javaTypes.length];
TypeConstructor[] visibleTypes = perspective.getTypeConstructors();
for (int i = 0; i < javaTypes.length; i++) {
calTypes[i] = mapToCALType(javaTypes[i], visibleTypes);
}
return calTypes;
}
}
/**
* A special JList for displaying the members of a Java class.
* This list uses a special renderer to highlight the tokens in the String
* representation of the class members and display special icons.
* @author Frank Worsley
*/
class JavaMemberList extends JList {
private static final long serialVersionUID = -6594368143989165773L;
/** The class the list is displaying members for. */
private Class<?> currentClass = null;
/** Whether only constructors are shown in the list. */
private boolean onlyShowConstructors = false;
/**
* Constructor for a new list.
*/
public JavaMemberList() {
super(new DefaultListModel());
setCellRenderer(new JavaMemberListCellRenderer());
setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
setToolTipText("JavaMemberList");
DefaultListModel listModel = (DefaultListModel) getModel();
listModel.addElement(GeneratorMessages.getString("JGF_InvalidClassName"));
}
/**
* @return the class the list is displaying members for or null if no current class.
*/
public Class<?> getCurrentClass() {
return currentClass;
}
/**
* @param onlyShowConstructors whether to show only constructors in the member list
*/
public void setOnlyShowConstructors(boolean onlyShowConstructors) {
this.onlyShowConstructors = onlyShowConstructors;
updateForClass(getCurrentClass());
}
/**
* Update the members displayed in the list for the given class.
* If the class is null then display an error message.
* @param newClass the class to update for
*/
public void updateForClass(Class<?> newClass) {
this.currentClass = newClass;
DefaultListModel listModel = new DefaultListModel();
if (currentClass == null) {
listModel.addElement(GeneratorMessages.getString("JGF_InvalidClassName"));
setModel(listModel);
return;
}
List<AccessibleObject> items = new ArrayList<AccessibleObject>();
Constructor<?>[] constructors = currentClass.getConstructors();
for (final Constructor<?> constructor : constructors) {
if (Modifier.isPublic(constructor.getModifiers())) {
items.add(constructor);
}
}
if (!onlyShowConstructors) {
Method[] methods = currentClass.getMethods();
for (final Method method : methods) {
if (Modifier.isPublic(method.getModifiers())) {
items.add(method);
}
}
Field[] fields = currentClass.getFields();
for (final Field field : fields) {
if (Modifier.isPublic(field.getModifiers())) {
items.add(field);
}
}
}
Collections.sort(items, new MemberListItemSorter());
for (final AccessibleObject accessibleObject : items) {
listModel.addElement(accessibleObject);
}
setModel(listModel);
}
/**
* @param e the MouseEvent for which to get the tooltip text
* @return the tooltip text for the list item at the coordinates or null if there is no
* item at the coordinates
*/
public String getToolTipText(MouseEvent e) {
int index = locationToIndex(e.getPoint());
if (index == -1) {
return null;
}
Object value = getModel().getElementAt(index);
if (value == null) {
return null;
} else if (value instanceof String) {
return value.toString();
}
String basicString = JavaMemberListCellRenderer.getValueString(value);
String[] tokens = basicString.split(" ", 2);
if (tokens.length == 0) {
return basicString;
} else {
return "<html><body><b>" + tokens[0] + "</b> <i>" + tokens[1] + "</i></body></html>";
}
}
/**
* We want tooltips to be displayed to the right of an item.
* If there is no item at the coordinates it returns null.
* @param e the mouse event for which to determine the location
* @return the tooltip location for the list item at the coordinates of the mouse event
*/
public Point getToolTipLocation(MouseEvent e) {
int index = locationToIndex(e.getPoint());
if (index == -1) {
return null;
}
Rectangle cellBounds = getCellBounds(index, index);
// take off 50 and add 5 for good looks
return new Point (cellBounds.x + cellBounds.width - 50, cellBounds.y + 5);
}
}
/**
* A custom list cell renderer for the Java members list that highlights names and uses custom icons.
* @author Frank Worsley
*/
class JavaMemberListCellRenderer extends DefaultListCellRenderer {
private static final long serialVersionUID = -2498691767497372110L;
/** The icon to use for constructors. */
private static final Icon CONSTRUCTOR_ICON = new ImageIcon(GemCutter.class.getResource("/Resources/javaConstructor.gif"));
/** The icon to use for static methods. */
private static final Icon STATIC_METHOD_ICON = new ImageIcon(GemCutter.class.getResource("/Resources/javaStaticMethod.gif"));
/** The icon to use for instance methods. */
private static final Icon INSTANCE_METHOD_ICON = new ImageIcon(GemCutter.class.getResource("/Resources/javaInstanceMethod.gif"));
/** The icon to use for static fields. */
private static final Icon STATIC_FIELD_ICON = new ImageIcon(GemCutter.class.getResource("/Resources/javaStaticField.gif"));
/** The icon to use for instance fields. */
private static final Icon INSTANCE_FIELD_ICON = new ImageIcon(GemCutter.class.getResource("/Resources/javaInstanceField.gif"));
/** The value we are rendering. */
private Object value = null;
/** Whether the cell being rendered is selected. */
private boolean isSelected = false;
/** The list the cell is being renderer for. */
private JList list = null;
/**
* @see javax.swing.ListCellRenderer#getListCellRendererComponent(javax.swing.JList, java.lang.Object, int, boolean, boolean)
*/
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
this.list = list;
this.value = value;
this.isSelected = isSelected;
super.getListCellRendererComponent(list, getValueString(value), index, isSelected, cellHasFocus);
// Set a proper icon
if (value instanceof Constructor<?>) {
setIcon(CONSTRUCTOR_ICON);
} else if (value instanceof Method) {
if (Modifier.isStatic(((Method) value).getModifiers())) {
setIcon(STATIC_METHOD_ICON);
} else {
setIcon(INSTANCE_METHOD_ICON);
}
} else if (value instanceof Field) {
if (Modifier.isStatic(((Field) value).getModifiers())) {
setIcon(STATIC_FIELD_ICON);
} else {
setIcon(INSTANCE_FIELD_ICON);
}
}
// For anything other than error strings we draw custom highlighted text.
// So just set foreground to be the same as background and nothing will be drawn at all.
if (!(value instanceof String)) {
setForeground(getBackground());
}
return this;
}
/**
* @return the x-coordinate at which point the text portion of the list cell starts.
*/
private int getLabelStart() {
Icon icon = getIcon();
if (icon != null) {
return icon.getIconWidth() + Math.max(1, getIconTextGap());
}
return 0;
}
/**
* Paint custom highlighted text for the items in the list.
* @see java.awt.Component#paint(java.awt.Graphics)
*/
public void paint(Graphics g) {
super.paint(g);
// Nothing special to do for error messages.
if (value instanceof String) {
return;
}
// Replace with our custom text.
String valueString = getText();
AttributedString customString = new AttributedString(valueString);
// By default draw the text in the normal font and color
customString.addAttribute(TextAttribute.FOREGROUND, isSelected ? list.getSelectionForeground() : list.getForeground());
customString.addAttribute(TextAttribute.FONT, getFont());
// Highlight the method/constructor/field name
int nameEnd = valueString.indexOf(" ", 0);
if (nameEnd == -1) {
nameEnd = valueString.length();
}
Color highlight = value instanceof Field ? Color.BLUE : Color.GREEN.darker();
customString.addAttribute(TextAttribute.FOREGROUND, highlight, 0, nameEnd);
// Draw the rest of the text in gray and italics
customString.addAttribute(TextAttribute.FOREGROUND, Color.GRAY, nameEnd + 1, valueString.length());
customString.addAttribute(TextAttribute.FONT, getFont().deriveFont(Font.ITALIC), nameEnd + 1, valueString.length());
g.drawString(customString.getIterator(), getLabelStart(), getHeight() - 3);
}
/**
* @param value the value to get a string representation for
* @return the String used to represent the value in the list
*/
public static String getValueString(Object value) {
if (value instanceof Constructor<?>) {
Constructor<?> constructor = (Constructor<?>) value;
Class<?>[] parameters = constructor.getParameterTypes();
return getClassName(constructor.getDeclaringClass()) + " " + getSignatureString(parameters);
} else if (value instanceof Method) {
Method method = (Method) value;
Class<?>[] parameters = method.getParameterTypes();
String staticString = Modifier.isStatic(method.getModifiers()) ? "static " : "";
return method.getName() + " " + getSignatureString(parameters) + " - " + staticString + getClassName(method.getReturnType());
} else if (value instanceof Field) {
Field field = (Field) value;
String staticString = Modifier.isStatic(field.getModifiers()) ? "static " : "";
return field.getName() + " - " + staticString + getClassName(field.getType());
} else {
return value.toString();
}
}
/**
* @param parameters the array of Class objects to generate a signature string for.
* @return a method signature string for the array of parameters
*/
private static String getSignatureString(Class<?>[] parameters) {
StringBuilder text = new StringBuilder();
text.append("(");
for (final Class<?> parameter : parameters) {
text.append(getClassName(parameter));
text.append(", ");
}
if (parameters.length > 0) {
// Remove trailing comma
text.delete(text.length() - 2, text.length());
}
text.append(")");
return text.toString();
}
/**
* @param parameter the class to get a name for
* @return an unqualified, proper name for the class (arrays get [] appened to their element type)
*/
public static String getClassName(Class<?> parameter) {
if (parameter.isArray()) {
return getClassName(parameter.getComponentType()) + "[]";
} else {
String[] tokens = parameter.getName().split("\\.");
return tokens[tokens.length - 1];
}
}
}
/**
* Sorts the items in the java member list. Constructors go first, then methods, then fields.
* For items of the same type it uses case-insensitive sorting.
* @author Frank Worsley
*/
class MemberListItemSorter implements Comparator<AccessibleObject> {
public int compare(AccessibleObject o1, AccessibleObject o2) {
// Constructors come before anything else.
if (o1 instanceof Constructor<?>) {
if (o2 instanceof Constructor<?>) {
return compareString(o1, o2);
} else {
return -1;
}
}
// Methods come after constructors, but before fields.
if (o1 instanceof Method) {
if (o2 instanceof Constructor<?>) {
return 1;
} else if (o2 instanceof Field) {
return -1;
} else {
return compareString(o1, o2);
}
}
// Fields come after everything else.
if (o1 instanceof Field) {
if (o2 instanceof Field) {
return compareString(o1, o2);
} else {
return 1;
}
}
throw new IllegalStateException("invalid value in the list");
}
private int compareString(AccessibleObject o1, AccessibleObject o2) {
return JavaMemberListCellRenderer.getValueString(o1).compareToIgnoreCase(JavaMemberListCellRenderer.getValueString(o2));
}
}