/* A top-level dialog window for configuring the ports of an entity. Copyright (c) 1998-2014 The Regents of the University of California. All rights reserved. Permission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, and distribute this software and its documentation for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software. IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. PT_COPYRIGHT_VERSION_2 COPYRIGHTENDKEY */ package ptolemy.actor.gui; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Frame; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Vector; import javax.swing.AbstractAction; import javax.swing.DefaultCellEditor; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFormattedTextField; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.table.AbstractTableModel; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import ptolemy.actor.Actor; import ptolemy.actor.IOPort; import ptolemy.actor.TypeAttribute; import ptolemy.actor.TypedActor; import ptolemy.actor.TypedIOPort; import ptolemy.data.BooleanToken; import ptolemy.data.Token; import ptolemy.data.expr.ASTPtRootNode; import ptolemy.data.expr.Parameter; import ptolemy.data.expr.ParseTreeEvaluator; import ptolemy.data.expr.PtParser; import ptolemy.data.type.TypeLattice; import ptolemy.graph.DirectedAcyclicGraph; import ptolemy.gui.PtGUIUtilities; import ptolemy.kernel.Entity; import ptolemy.kernel.Port; import ptolemy.kernel.undo.UndoChangeRequest; import ptolemy.kernel.util.Attribute; import ptolemy.kernel.util.ChangeListener; import ptolemy.kernel.util.ChangeRequest; import ptolemy.kernel.util.IllegalActionException; import ptolemy.kernel.util.InternalErrorException; import ptolemy.kernel.util.NamedObj; import ptolemy.kernel.util.StringAttribute; import ptolemy.moml.MoMLChangeRequest; import ptolemy.moml.unit.ParseException; import ptolemy.moml.unit.UnitAttribute; import ptolemy.moml.unit.UnitLibrary; import ptolemy.util.MessageHandler; import ptolemy.util.StringUtilities; /////////////////////////////////////////////////////////////////// //// PortConfigurerDialog /** This class is a non-modal dialog for configuring the ports of an entity. The columns of the dialog displayed depend on the type of the Entity (target) for which we are configuring the ports. By default, "Name", "Direction, "Show Name", and "Hide" are displayed for all target types. We assume that the ports are of type Port or ComponentPort. If the target is an Actor, then the ports are of type IOPort and we add the "Input", "Output", and "Multiport" columns. If the target is a TypedActor, then the ports are of type TypedIOPort, and we add the "Type" and "Units" columns. NOTE: This code checks for the existence of each column that may be used, but it sometimes assumes the existence of the "Name" column. @author Rowland R Johnson, Elaine Cheong @version $Id: PortConfigurerDialog.java 70402 2014-10-23 00:52:20Z cxh $ @since Ptolemy II 1.0 @Pt.ProposedRating Yellow (eal) @Pt.AcceptedRating Red (eal) */ @SuppressWarnings("serial") public class PortConfigurerDialog extends PtolemyDialog implements ChangeListener { /** * Construct a dialog that presents the ports as a table. Each row of the * table corresponds to one port. The user modifies the table to specify * changes in the ports. When the apply button is pressed the contents of * the table is used to update the ports. When Commit is pressed an apply * is done before exiting. *
* This dialog is is not modal. In particular, changes can be undone by
* clicking Edit->Undo, and the help screen can be manipulated while this
* dialog exists. The dialog is placed relative to the owner.
*
* @param tableau The DialogTableau.
* @param owner The object that, per the user, appears to be generating the
* dialog.
* @param target The object whose ports are being configured.
* @param configuration The configuration to use to open the help screen
* (or null if help is not supported).
*/
public PortConfigurerDialog(DialogTableau tableau, Frame owner,
Entity target, Configuration configuration) {
super("Configure ports for " + target.getName(), tableau, owner,
target, configuration);
// Listen for changes that may need to be reflected in the table.
getTarget().addChangeListener(this);
// Create the JComboBox that used to select the location of the port
_portLocationComboBox = new JComboBox();
_portLocationComboBox.addItem("DEFAULT");
_portLocationComboBox.addItem("NORTH");
_portLocationComboBox.addItem("EAST");
_portLocationComboBox.addItem("SOUTH");
_portLocationComboBox.addItem("WEST");
_portTable = new JTable();
// If you change the height, then check that a few rows can be added.
// Also, check the setRowHeight call below.
_portTable.setPreferredScrollableViewportSize(new Dimension(600, 100));
_portTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent mouseEvent) {
if (PtGUIUtilities.macOSLookAndFeel()
&& (mouseEvent.isPopupTrigger() || mouseEvent
.getButton() == MouseEvent.BUTTON1
&& (mouseEvent.getModifiersEx() | java.awt.event.InputEvent.CTRL_MASK) == java.awt.event.InputEvent.CTRL_MASK)
|| !PtGUIUtilities.macOSLookAndFeel()
&& mouseEvent.getButton() == MouseEvent.BUTTON3) {
Point point = mouseEvent.getPoint();
int row = _portTable.rowAtPoint(point);
_setSelectedRow(row);
}
}
});
_portTable.addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent ke) {
if (ke.getKeyChar() == '\n') {
if (_apply()) {
_cancel();
}
}
}
});
_portTable.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent event) {
// Set the selected row so the remove key gets updated
_setSelectedRow(_portTable.getSelectionModel()
.getAnchorSelectionIndex());
}
@Override
public void focusLost(FocusEvent event) {
}
});
// Initialize which columns will be visible for this target.
_initColumnNames();
// Create the TableModel and set certain cell editors and renderers
_setupTableModel();
// Initialize the displayed column widths.
_initColumnSizes();
// Make the contents of the table scrollable
setScrollableContents(_portTable);
// The following sets up a listener for mouse clicks on the
// header cell of the Show Name column. A click causes the
// values in this column to all change.
// FIXME: this doesn't seem to work if you click multiple
// times in a session
_jth = _portTable.getTableHeader();
if (_columnNames.contains(ColumnNames.COL_SHOW_NAME)
|| _columnNames.contains(ColumnNames.COL_HIDE)) {
_jth.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent me) {
// indexOf() returns -1 if element is not in ArrayList
int showName = _columnNames
.indexOf(ColumnNames.COL_SHOW_NAME);
if (showName != -1) {
Rectangle headerShowNameRect = _jth
.getHeaderRect(showName);
if (headerShowNameRect.contains(me.getPoint())) {
_portTableModel.toggleShowAllNames();
}
}
int hide = _columnNames.indexOf(ColumnNames.COL_HIDE);
if (hide != 1) {
Rectangle headerHidePortRect = _jth.getHeaderRect(hide);
if (headerHidePortRect.contains(me.getPoint())) {
_portTableModel.toggleHidePorts();
}
}
}
});
}
pack();
setVisible(true);
}
///////////////////////////////////////////////////////////////////
//// public variables ////
/** The background color of an uneditable cell. */
public static final Color UNEDITABLE_CELL_COLOR = new Color(255, 128, 128);
///////////////////////////////////////////////////////////////////
//// public methods ////
/**
* Notify the listener that a change has been successfully executed.
*
* @param change The change that has been executed.
*/
@Override
public void changeExecuted(ChangeRequest change) {
// Ignore if this is the originator or if this is a change
// from above that is anything other than an undo. Detecting
// that it is an undo from above seems awkward. A better way
// would be to extend the ChangeRequest system to include
// ChangeRequest types so that an undo would be explicitly
// represented.
if (change == null || change.getSource() == this
|| !(change instanceof UndoChangeRequest)) {
return;
}
// The ports of the _target may have changed.
_setupTableModel();
}
/**
* Notify the listener that a change has resulted in an exception.
*
* @param change The change that was attempted.
* @param exception The exception that resulted.
*/
@Override
public void changeFailed(ChangeRequest change, Exception exception) {
// TODO Determine best way to handle failed change
// request. This method _should_ never be invoked if the
// source is this. For now, at least, test to see if the
// source is this, and report it.
if (change == null) {
return;
}
if (!change.isErrorReported()) {
MessageHandler.error("Change failed: ", exception);
}
}
/** Close this dialog. If the state has not be saved, prompt
* the user to save the modifications.
* @return false if the user selects cancel, otherwise return true.
*/
public boolean close() {
if (_isDirty()) {
int option = JOptionPane.showConfirmDialog(getOwner(),
"Save port modifications on " + getTarget().getFullName(),
"Unsaved Port Modifications",
JOptionPane.YES_NO_CANCEL_OPTION);
switch (option) {
case JOptionPane.YES_OPTION: {
_apply();
return true;
}
case JOptionPane.NO_OPTION:
return true;
case JOptionPane.CANCEL_OPTION:
return false;
}
}
return true;
}
@Override
public void saveIfRequired() {
if (_isDirty()) {
int option = JOptionPane.showConfirmDialog(getOwner(),
"Save port modifications on " + getTarget().getFullName()
+ "?", "Unsaved Port Modifications",
JOptionPane.YES_NO_OPTION);
switch (option) {
case JOptionPane.YES_OPTION:
_apply();
}
}
}
///////////////////////////////////////////////////////////////////
//// protected methods ////
/** Apply any changes that may have been made in the table.
* @return true if the change was successfully applied
*/
protected boolean _apply() {
// The port names in the table will be used many times, so extract
// them here.
String[] portNameInTable = new String[_portTableModel.getRowCount()];
for (int i = 0; i < _portTableModel.getRowCount(); i++) {
portNameInTable[i] = (String) _portTableModel.getValueAt(i,
_columnNames.indexOf(ColumnNames.COL_NAME));
}
// Do some basic checks on table for things that are obviously
// incorrect. First, make sure all the new ports have names
// other than the empty string.
for (int i = 0; i < _portTableModel.getRowCount(); i++) {
if (portNameInTable[i].equals("")) {
JOptionPane.showMessageDialog(this,
"All Ports need to have a name.");
return false;
}
}
// Now, make sure all port names are unique.
for (int i = 0; i < _portTableModel.getRowCount(); i++) {
for (int j = i + 1; j < _portTableModel.getRowCount(); j++) {
if (portNameInTable[i].equals(portNameInTable[j])) {
JOptionPane.showMessageDialog(this, portNameInTable[i]
+ " is a duplicate port name.\n"
+ "Please remove all but one");
return false;
}
}
}
// Determine which ports have been removed. If a port exists on the
// target but is not represented by a row in the table then it needs
// to be removed.
Vector portsToBeRemoved = new Vector();
Iterator portIterator = getTarget().portList().iterator();
Port actualPort = null;
while (portIterator.hasNext()) {
Object candidate = portIterator.next();
if (candidate instanceof Port) {
boolean foundPort = false;
actualPort = (Port) candidate;
for (int i = 0; i < _ports.size(); i++) {
Hashtable portInfo = (Hashtable) _ports.elementAt(i);
if (actualPort == (Port) portInfo
.get(ColumnNames.COL_ACTUAL_PORT)) {
foundPort = true;
break;
}
}
if (!foundPort) {
portsToBeRemoved.add(actualPort);
}
} else {
Exception exception = new InternalErrorException(
"The target portList contains" + " an object \""
+ candidate + "\"that is not of type Port.");
MessageHandler.error("Internal Error while removing a port.",
exception);
}
}
Iterator actualPorts = portsToBeRemoved.iterator();
while (actualPorts.hasNext()) {
StringBuffer moml = new StringBuffer();
actualPort = (Port) actualPorts.next();
// The context for the MoML should be the first container
// above this port in the hierarchy that defers its MoML
// definition, or the immediate parent if there is none.
NamedObj container = actualPort.getContainer();
NamedObj composite = container.getContainer();
if (composite != null) {
moml.append(" Based on IntegerEditor from
http://download.oracle.com/javase/tutorial/uiswing/examples/components/TableFTFEditDemoProject/src/components/IntegerEditor.java
@author Christopher Brooks, Sun Microsystems
@version $Id: PortConfigurerDialog.java 70402 2014-10-23 00:52:20Z cxh $
@since Ptolemy II 5.1
@Pt.ProposedRating Red (eal)
@Pt.AcceptedRating Red (eal)
*/
public class ValidatingJTextFieldCellEditor extends DefaultCellEditor {
/** Construct a validating JTextField JTable Cell editor.
*/
public ValidatingJTextFieldCellEditor() {
super(new JFormattedTextField());
}
/** Construct a validating JTextField JTable Cell editor.
* @param jFormattedTextField The JTextField that provides choices.
*/
public ValidatingJTextFieldCellEditor(
final JFormattedTextField jFormattedTextField) {
super(jFormattedTextField);
_jFormattedTextField = (JFormattedTextField) getComponent();
// React when the user presses Enter while the editor is
// active. (Tab is handled as specified by
// JFormattedTextField's focusLostBehavior property.)
jFormattedTextField.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "check");
jFormattedTextField.getActionMap().put("check",
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
boolean valid = true;
if (_validator != null) {
valid = _validator.isValid(jFormattedTextField
.getText());
}
if (!valid) {
userSaysRevert(jFormattedTextField.getText());
} else {
jFormattedTextField.postActionEvent(); //stop editing
}
}
});
_jFormattedTextField.addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent ke) {
_setDirty(true);
_enableApplyButton(true);
if (ke.getKeyChar() == '\n') {
if (_apply()) {
_cancel();
}
}
}
});
_jFormattedTextField.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent event) {
// Set the selected row so the remove key gets updated
_setSelectedRow(_portTable.getSelectionModel()
.getAnchorSelectionIndex());
}
@Override
public void focusLost(FocusEvent event) {
}
});
}
///////////////////////////////////////////////////////////////////
//// public methods ////
/**
*/
@Override
public Component getTableCellEditorComponent(JTable table,
Object value, boolean isSelected, int row, int column) {
JTextField jTextField = (JTextField) super
.getTableCellEditorComponent(table, value, isSelected, row,
column);
_oldValue = jTextField.getText();
jTextField.setText((String) value);
return jTextField;
}
/** Get the cell editor value.
* @return The string value of the selected item in the combobox.
*/
@Override
public Object getCellEditorValue() {
// FIXME: do we need to get jTextField like this each time?
JTextField jTextField = (JTextField) getComponent();
Object o = jTextField.getText();
return o.toString();
}
/** Set the validator.
* @param validator The validator.
*/
public void setValidator(CellValidator validator) {
_validator = validator;
}
/** Check the selection and determine whether we should stop editing.
* If the selection is invalid, ask the user if they want to revert.
* If the selection is valid, then call stopCellEditing in the super
* class
* @return False if the selection is invalid. Otherwise,
* return whatever super.stopCellEditing() returns.
*/
@Override
public boolean stopCellEditing() {
// FIXME: do we need to get jTextField like this each time?
JFormattedTextField jFormattedTextField = (JFormattedTextField) getComponent();
if (jFormattedTextField.getText() == null) {
// FIXME: why does the selected item get set to null sometimes?
jFormattedTextField.setText("");
}
boolean valid = true;
if (_validator != null) {
valid = _validator.isValid(jFormattedTextField.getText());
}
if (!valid) {
if (_userWantsToEdit) {
// User already selected edit, don't ask twice.
_userWantsToEdit = false;
return false;
} else {
if (!userSaysRevert(jFormattedTextField.getText())) {
_userWantsToEdit = true;
return false; //don't let the editor go away
}
}
}
return super.stopCellEditing();
}
///////////////////////////////////////////////////////////////////
//// protected methods ////
/** Return true if the user wants to revert to the original value.
* A dialog box pops up that tells the user that their selection
* is invalid.
* @param selectedItem The selected item.
* @return True if the user elects to revert to the last good
* value. Otherwise, returns false, indicating that the user
* wants to continue editing.
*/
protected boolean userSaysRevert(String selectedItem) {
Toolkit.getDefaultToolkit().beep();
_jFormattedTextField.selectAll();
Object[] options = { "Edit", "Revert" };
int answer = JOptionPane.showOptionDialog(
SwingUtilities.getWindowAncestor(_jFormattedTextField),
"The value \"" + selectedItem + "\" is not valid:\n"
+ _validator.getMessage()
+ "\nYou can either continue editing "
+ "or revert to the last valid value \""
+ _oldValue + "\".", "Invalid Text Entered",
JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null,
options, options[1]);
if (answer == 1) { //Revert!
_jFormattedTextField.setText((String) _oldValue);
return true;
}
return false;
}
///////////////////////////////////////////////////////////////////
//// private variables ////
/** The JTextField. */
private JFormattedTextField _jFormattedTextField;
/** Old value of the JTextField. */
private Object _oldValue;
/** True if the user wants to edit after having an invalid selection.*/
private boolean _userWantsToEdit;
/** Class that validates the cell. */
private CellValidator _validator;
}
/**
A validating ComboBox table cell editor for use with JTable.
To determine if a selection is valid, this class uses the
CellValidator class.
Based on IntegerEditor from
http://download.oracle.com/javase/tutorial/uiswing/examples/components/TableFTFEditDemoProject/src/components/IntegerEditor.java
@author Christopher Brooks, Sun Microsystems
@version $Id: PortConfigurerDialog.java 70402 2014-10-23 00:52:20Z cxh $
@since Ptolemy II 5.1
@Pt.ProposedRating Red (eal)
@Pt.AcceptedRating Red (eal)
*/
public static class ValidatingComboBoxCellEditor extends DefaultCellEditor {
// FindBugs suggested refactoring this into a static class.
/** Construct a validating combo box JTable Cell editor.
* @param comboBox The combo box that provides choices.
*/
public ValidatingComboBoxCellEditor(final JComboBox comboBox) {
super(comboBox);
_comboBox = (JComboBox) getComponent();
// React when the user presses Enter while the editor is
// active. (Tab is handled as specified by
// JFormattedTextField's focusLostBehavior property.)
comboBox.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "check");
comboBox.getActionMap().put("check", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
boolean valid = true;
if (_validator != null) {
valid = _validator.isValid((String) comboBox
.getSelectedItem());
}
if (!valid) {
userSaysRevert((String) comboBox.getSelectedItem());
}
}
});
}
///////////////////////////////////////////////////////////////////
//// public methods ////
/**
*/
@Override
public Component getTableCellEditorComponent(JTable table,
Object value, boolean isSelected, int row, int column) {
JComboBox comboBox = (JComboBox) super.getTableCellEditorComponent(
table, value, isSelected, row, column);
_oldValue = comboBox.getSelectedItem();
comboBox.setSelectedItem(value);
return comboBox;
}
/** Get the cell editor value.
* @return The string value of the selected item in the combobox.
*/
@Override
public Object getCellEditorValue() {
// FIXME: do we need to get comboBox like this each time?
JComboBox comboBox = (JComboBox) getComponent();
Object o = comboBox.getSelectedItem();
return o.toString();
}
/** Set the validator.
* @param validator The validator.
*/
public void setValidator(CellValidator validator) {
_validator = validator;
}
/** Check the selection and determine whether we should stop editing.
* If the selection is invalid, ask the user if they want to revert.
* If the selection is valid, then call stopCellEditing in the super
* class
* @return False if the selection is invalid. Otherwise,
* return whatever super.stopCellEditing() returns.
*/
@Override
public boolean stopCellEditing() {
// FIXME: do we need to get comboBox like this each time?
JComboBox comboBox = (JComboBox) getComponent();
if (comboBox.getSelectedItem() == null) {
// FIXME: why does the selected item get set to null sometimes?
comboBox.setSelectedItem("");
}
boolean valid = true;
if (_validator != null) {
valid = _validator.isValid((String) comboBox.getSelectedItem());
}
if (!valid) {
if (_userWantsToEdit) {
// User already selected edit, don't ask twice.
_userWantsToEdit = false;
return false;
} else {
if (!userSaysRevert((String) comboBox.getSelectedItem())) {
_userWantsToEdit = true;
return false; //don't let the editor go away
}
}
}
return super.stopCellEditing();
}
///////////////////////////////////////////////////////////////////
//// protected methods ////
/** Return true if the user wants to revert to the original value.
* A dialog box pops up that tells the user that their selection
* is invalid.
* @param selectedItem The selected item.
* @return True if the user elects to revert to the last good
* value. Otherwise, returns false, indicating that the user
* wants to continue editing.
*/
protected boolean userSaysRevert(String selectedItem) {
Toolkit.getDefaultToolkit().beep();
//_comboBox.selectAll();
Object[] options = { "Edit", "Revert" };
int answer = JOptionPane.showOptionDialog(
SwingUtilities.getWindowAncestor(_comboBox),
"The value \"" + selectedItem + "\" is not valid:\n"
+ _validator.getMessage()
+ "\nYou can either continue editing "
+ "or revert to the last valid value \""
+ _oldValue + "\".", "Invalid Text Entered",
JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null,
options, options[1]);
if (answer == 1) { //Revert!
_comboBox.setSelectedItem(_oldValue);
return true;
}
return false;
}
///////////////////////////////////////////////////////////////////
//// private variables ////
/** The combo box. */
private JComboBox _comboBox;
/** Old value of the combo box. */
private Object _oldValue;
/** True if the user wants to edit after having an invalid selection.*/
private boolean _userWantsToEdit;
/** Class that validates the cell. */
private CellValidator _validator;
}
///////////////////////////////////////////////////////////////////
//// private methods ////
/** Create the MoML expression that represents the update. */
private String _createMoMLUpdate(Hashtable updates, Hashtable portInfo,
String currentPortName, String newPortName) {
StringBuffer momlUpdate = new StringBuffer("