/* * 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. */ /* * ListTupleValueEditor.java * Created: Feb 16, 2001 * By: Michael Cheng */ package org.openquark.gems.client.valueentry; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.FlowLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.UIManager; import javax.swing.border.EtchedBorder; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import org.openquark.cal.compiler.FieldName; import org.openquark.cal.compiler.PreludeTypeConstants; import org.openquark.cal.compiler.RecordType; import org.openquark.cal.compiler.TypeConsApp; import org.openquark.cal.compiler.TypeExpr; import org.openquark.cal.valuenode.AbstractRecordValueNode; import org.openquark.cal.valuenode.ListOfCharValueNode; import org.openquark.cal.valuenode.ListValueNode; import org.openquark.cal.valuenode.LiteralValueNode; import org.openquark.cal.valuenode.NTupleValueNode; import org.openquark.cal.valuenode.RecordValueNode; import org.openquark.cal.valuenode.ValueNode; import org.openquark.cal.valuenode.ValueNodeBuilderHelper; import org.openquark.gems.client.GemCutter; /** * ValueEditor for editing lists, lists of tuples and lists of records. * @author Michael Cheng */ public class ListTupleValueEditor extends AbstractListValueEditor { /* * TODOEL: unify handling of lists of tuples and lists of records, using AbstractRecordValueNode. */ /** * This interface defines what drag operations are supported by the * <code>ListTupleValueEditor</code>. Implementors of this interface will be * called when a certain drag operation is initiated by the user. */ public interface ListTupleValueDragPointHandler extends ValueEditorDragPointHandler { /** * This method defines the behaviour when the user attempts to drag an entire list * of objects out from the <code>ListTupleValueEditor</code>. By default, this * method is empty and subclasses are encouraged to override this class to * specify their own drag handling code. * @param dge * @param parentEditor * @return boolean */ boolean dragList(DragGestureEvent dge, ListTupleValueEditor parentEditor); /** * This method defines the behaviour when the user attempts to drag a portion * of a tuple from the <code>ListTupleValueEditor</code>. By default, this * method is empty and subclasses are encouraged to override this class to * specify their own drag handling code. * @param dge * @param parentEditor * @param tupleElementIndex * @return boolean */ boolean dragTupleList( DragGestureEvent dge, ListTupleValueEditor parentEditor, int tupleElementIndex); } /** * A custom value editor provider for the ListTupleValueEditor. */ public static class ListTupleValueEditorProvider extends ValueEditorProvider<ListTupleValueEditor> { public ListTupleValueEditorProvider(ValueEditorManager valueEditorManager) { super(valueEditorManager); } /** * {@inheritDoc} */ @Override public boolean canHandleValue(ValueNode valueNode, SupportInfo providerSupportInfo) { return valueNode instanceof ListValueNode && canEditListType((ListValueNode) valueNode, providerSupportInfo); } /** * @see org.openquark.gems.client.valueentry.ValueEditorProvider#getEditorInstance(ValueEditorHierarchyManager, ValueNode) */ @Override public ListTupleValueEditor getEditorInstance(ValueEditorHierarchyManager valueEditorHierarchyManager, ValueNode valueNode) { ListTupleValueEditor editor = new ListTupleValueEditor(valueEditorHierarchyManager, null); editor.setOwnerValueNode(valueNode); return editor; } /** * {@inheritDoc} */ @Override public ListTupleValueEditor getEditorInstance( ValueEditorHierarchyManager valueEditorHierarchyManager, ValueNodeBuilderHelper valueNodeBuilderHelper, ValueEditorDragManager dragManager, ValueNode valueNode) { ListTupleValueEditor editor = new ListTupleValueEditor(valueEditorHierarchyManager, getListTupleDragPointHandler(dragManager)); editor.setOwnerValueNode(valueNode); return editor; } /** * {@inheritDoc} */ @Override public boolean usableForOutput() { return true; } /** * Checks if the type of the list elements is supported by value editors. * @param valueNode the list value node * @param providerSupportInfo * @return true if the type of the list elements is supported */ private boolean canEditListType(ListValueNode valueNode, SupportInfo providerSupportInfo) { TypeConsApp listTypeConsApp = valueNode.getTypeExpr().rootTypeConsApp(); TypeExpr typeConsArg = listTypeConsApp.getArg(0); return getValueEditorManager().canEditInputType(typeConsArg, providerSupportInfo); } /** * A convenient method for casting the drag point handler to the type that is * suitable for the <code>ListTupleValueEditor</code> to use. If such conversion is not * possible, then this method should return <code>null</code>. * @param dragManager * @return ListTupleDragPointHandler */ private ListTupleValueDragPointHandler getListTupleDragPointHandler(ValueEditorDragManager dragManager) { ValueEditorDragPointHandler handler = getDragPointHandler(dragManager); if (handler instanceof ListTupleValueDragPointHandler) { return (ListTupleValueDragPointHandler) handler; } return null; } } private class ListTupleDragGestureListener implements DragGestureListener { /** * @see java.awt.dnd.DragGestureListener#dragGestureRecognized(java.awt.dnd.DragGestureEvent) */ public void dragGestureRecognized(DragGestureEvent dge) { if (dge.getTriggerEvent() instanceof MouseEvent) { JTableHeader header = getTableHeader(); // If we are currently resizing a column then we shouldn't initiate a drag action if (header.getResizingColumn() == null) { TableColumnModel model = header.getColumnModel(); TypeExpr listElementType = getListElementType(); if (listElementType.rootRecordType() != null) { // A list of values which are records. if (getListElementType().isTupleType()) { int xPos = ((MouseEvent)dge.getTriggerEvent()).getX(); dragFromListOfTuples(dge, model.getColumnIndexAtX(xPos)); } else { // A list of records which aren't tuples. } } else { // A list of values which aren't records. dragFromList(dge); } } } } /** * This should only be called if the type expression is a list of elements. * It will initiate a drag event appropriate for dragging the data out of the value editor. * @param dge DragGestureEvent - The gesture that started it all */ private void dragFromList(DragGestureEvent dge) { if (dragPointHandler != null) { dragPointHandler.dragList(dge, ListTupleValueEditor.this); } } /** * This should only be called for a list of tuples. It will initiates a drag event appropriate * for dragging the specified tuple element of the tuple * @param dge DragGestureEvent - The gesture that started it all * @param element int - The tuple index to be dragged out */ private void dragFromListOfTuples(DragGestureEvent dge, int element) { if (dragPointHandler != null) { dragPointHandler.dragTupleList(dge, ListTupleValueEditor.this, element); } } } /** * Renderer used to show empty table cells. * Specifically, this is used to render the unconsolidated column cells for a record with * no fields. * @author Iulian Radu */ static class EmptyCellRenderer extends DefaultTableCellRenderer { private static final long serialVersionUID = 857633457177524459L; EmptyCellRenderer () { } /** * {@inheritDoc} */ @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (isSelected) { setBackground(Color.LIGHT_GRAY); } else { setBackground(UIManager.getColor("Panel.background")); } setBorder(BorderFactory.createLineBorder(Color.GRAY)); setText((value == null) ? "" : value.toString()); return this; } } // Allow this value editor to initiate drag events private final ListTupleValueDragPointHandler dragPointHandler; /** Icon for the consolidate columns button */ private final static ImageIcon consolidateColumnsIcon = new ImageIcon(GemCutter.class.getResource("/Resources/consolidateColumns.gif")); /** Icon for the add field button */ private final static ImageIcon addColumnIcon = new ImageIcon(GemCutter.class.getResource("/Resources/addColumn.gif")); /** Icon for the remove field button */ private final static ImageIcon deleteColumnIcon = new ImageIcon(GemCutter.class.getResource("/Resources/deleteColumn.gif")); /** * Whether columns are being consolidated. Only applicable (and potentially true) if editing a list of records or tuples. * If True, the list elements appear in one column; if False, tuple and record fields appear in * separate columns. */ private boolean consolidateColumns = false; /** Whether the editor has been initialized once */ private boolean initializedOnce; /** Button for consolidating columns */ private JButton consolidateColumnsButton; /** Button for adding a new record field */ private JButton addFieldButton; /** Button for removing a record field */ private JButton deleteFieldButton; /** Action of setting the column consolidation */ private Action switchConsolidationAction; /** Action of adding a record field */ private Action addFieldAction; /** Action of removing a record field */ private Action deleteFieldAction; /** Editable header used when the table represents a list of records in non-consolidated column layout*/ private RecordValueEditor.EditableHeader editableRecordHeader = null; /** * ListTupleValueEditor constructor comment. * @param valueEditorHierarchyManager the hierarchy manager for the editor * @param dragPointHandler a drag point handler if drag and drop should be enabled (can be null) */ protected ListTupleValueEditor(final ValueEditorHierarchyManager valueEditorHierarchyManager, ListTupleValueDragPointHandler dragPointHandler) { super(valueEditorHierarchyManager); this.dragPointHandler = dragPointHandler; // Attach a listener for drag events if (dragPointHandler != null) { JTableHeader tableHeader = getTableHeader(); DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer( tableHeader, DnDConstants.ACTION_COPY_OR_MOVE, new ListTupleDragGestureListener()); } // Initialize our buttons getConsolidateColumnsButton().addKeyListener(new AbstractListValueEditor.ListTupleValueEditorKeyListener()); getAddFieldButton().addKeyListener(new AbstractListValueEditor.ListTupleValueEditorKeyListener()); getDeleteFieldButton().addKeyListener(new AbstractListValueEditor.ListTupleValueEditorKeyListener()); getConsolidateColumnsButton().setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); getAddFieldButton().setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); getDeleteFieldButton().setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); // Initialize the table model to create columns with editable headers table.setColumnModel(new RecordValueEditor.EditableHeader.EditableHeaderTableColumnModel()); // Create the list of records editable header and record model // (these are created here but only used for lists of records) RecordValueEditor.RecordFieldModel recordFieldModel = new RecordValueEditor.RecordFieldModel(); RecordValueEditor.RowHeader.RowHeaderOwner rowHeaderOwner = new RecordValueEditor.RowHeader.RowHeaderOwner() { public TypeExpr getFieldType(int row) { FieldName fieldName = (getListElementType().rootRecordType().getHasFieldNames().get(row)); return getListElementType().rootRecordType().getHasFieldType(fieldName); } public void renameField(FieldName oldName, FieldName newName) { if (getListElementType().rootRecordType() == null) { throw new IllegalStateException(); } ListValueNode listValueNode = (ListValueNode)getValueNode(); int listSize = listValueNode.getNElements(); if (listSize > 0 && listValueNode.getValueAt(0) instanceof NTupleValueNode) { // list of tuple value nodes. // Check if renaming causes a list of tuple to be converted to a list of record. // True if renaming a list of tuple, unless the name really didn't change. if (!oldName.equals(newName)) { // Have to convert a list of NTuples to a list of records. // Note: It might be better to move some of this code to nTupleValueNode.transmute(), // or ValueNodeTransformer.transform(). This duplicates some work though. ValueNodeBuilderHelper valueNodeBuilderHelper = valueEditorHierarchyManager.getValueEditorManager().getValueNodeBuilderHelper(); RecordValueNode.RecordValueNodeProvider recordVNProvider = new RecordValueNode.RecordValueNodeProvider(valueNodeBuilderHelper); // Get the new record type. TypeExpr recordTypeExpr = AbstractRecordValueNode.getRecordTypeForRenamedField(getListElementType(), oldName, newName); // Iterate through the list and convert tuple nodes to record nodes. for (int i = 0; i < listSize; i++) { // Get the tuple node. NTupleValueNode nTupleValueNode = (NTupleValueNode)listValueNode.getValueAt(i); // Get the list of values. List<ValueNode> fieldValueList = new ArrayList<ValueNode>(nTupleValueNode.getValue()); // Create and set the record value node with the list of values. ValueNode recordValueNode = recordVNProvider.getNodeInstance(fieldValueList, null, recordTypeExpr); ((ListValueNode)getValueNode()).setValueNodeAt(i, recordValueNode); } } else { // No conversion is necessary. } } else { // Empty list, or list of record value nodes. // Iterate through the list of record nodes and change their type expressions for (int i = 0; i < listSize; i++) { AbstractRecordValueNode recordValueNode = (AbstractRecordValueNode)((ListValueNode)getValueNode()).getValueAt(i); recordValueNode = recordValueNode.renameField(oldName, newName, valueEditorManager.getValueNodeBuilderHelper(), valueEditorManager.getValueNodeTransformer()); ((ListValueNode)getValueNode()).setValueNodeAt(i, recordValueNode); } } // Now change the list element type expression and update the value node Map<FieldName, TypeExpr> fieldNamesToTypeMap = new HashMap<FieldName, TypeExpr>(); List<FieldName> existingFields = getListElementType().rootRecordType().getHasFieldNames(); for (final FieldName existingFieldName : existingFields) { TypeExpr existingFieldType = getListElementType().rootRecordType().getHasFieldType(existingFieldName); fieldNamesToTypeMap.put(existingFieldName, existingFieldType); } TypeExpr fieldTypeExpr = fieldNamesToTypeMap.remove(oldName); fieldNamesToTypeMap.put(newName, fieldTypeExpr); RecordType newRecordType = TypeExpr.makeNonPolymorphicRecordType(fieldNamesToTypeMap); ValueNodeBuilderHelper valueNodeBuilderHelper = valueEditorManager.getValueNodeBuilderHelper(); replaceValueNode(getValueNode().transmuteValueNode(valueNodeBuilderHelper, valueEditorManager.getValueNodeTransformer(), valueNodeBuilderHelper.getPreludeTypeConstants().makeListType(newRecordType)), true); // Select the column of the new field name int fieldIndex = getListElementType().rootRecordType().getHasFieldNames().indexOf(newName); if (getRowCount() > 0) { selectCell(0, fieldIndex); } } }; editableRecordHeader = new RecordValueEditor.EditableHeader(table.getColumnModel(), this, rowHeaderOwner, recordFieldModel); editableRecordHeader.addFocusListener(new FocusListener() { public void focusGained(FocusEvent e) { ListTupleValueEditor.this.clearSelection(true); enableButtonsForTableState(); } public void focusLost(FocusEvent e) { } }); } /** * @see org.openquark.gems.client.valueentry.AbstractListValueEditor#createListTableModel(org.openquark.cal.valuenode.ValueNode) */ @Override protected AbstractListTableModel createListTableModel(ValueNode valueNode) { return new ListTupleTableModel((ListValueNode) valueNode, valueEditorHierarchyManager, consolidateColumns); } /** * Get a map from every value node managed by this editor to its least constrained type. * @return Map from every value node managed by this editor to its least constrained type. */ private Map<ValueNode, TypeExpr> getValueNodeToUnconstrainedTypeMap() { Map<ValueNode, TypeExpr> returnMap = new HashMap<ValueNode, TypeExpr>(); // Get the value nodes for the list and the items in the list. ListValueNode listValueNode = (ListValueNode)getValueNode(); List<ValueNode> listElementNodes = listValueNode.getValue(); // Determine whether we are dealing with a generic list, list of tuples or list of records ValueNode firstChild = listElementNodes.get(0); boolean isListOfNTupleVNs = firstChild instanceof NTupleValueNode; // isListRecord will also be true. boolean isListRecord = getListElementType().rootRecordType() != null; int tupleSize = isListOfNTupleVNs ? ((NTupleValueNode)listElementNodes.get(0)).getTupleSize() : -1; // Calculate new unconstrained types for the list and the elements of the list. TypeExpr unconstrainedListType = getContext().getLeastConstrainedTypeExpr(); TypeExpr unconstrainedListElementType; if (unconstrainedListType.rootTypeVar() != null || (!consolidateColumns && isListRecord && unconstrainedListType.rootTypeConsApp().getArg(0).rootTypeVar() != null)) { // check for [a] // The context is parametric (ie: a or [a]), so create the least constrained type // depending on the list element type: if (!consolidateColumns && isListOfNTupleVNs) { // Tuple least constrained type: (a, a, ... ) unconstrainedListElementType = TypeExpr.makeTupleType(tupleSize); } else if (!consolidateColumns && isListRecord) { // Record least constrained type: {field1 = a, field2 = b, ...} // Build this from the existing fields List<FieldName> fieldNames = getListElementType().rootRecordType().getHasFieldNames(); Map<FieldName, TypeExpr> fieldNameToTypeMap = new HashMap<FieldName, TypeExpr>(); for (final FieldName fieldName : fieldNames) { fieldNameToTypeMap.put(fieldName, TypeExpr.makeParametricType()); } unconstrainedListElementType = TypeExpr.makeNonPolymorphicRecordType(fieldNameToTypeMap); } else { // General element least constrained type: (a) unconstrainedListElementType = TypeExpr.makeParametricType(); } unconstrainedListType = valueEditorManager.getValueNodeBuilderHelper().getPreludeTypeConstants().makeListType(unconstrainedListElementType); } else { // Element type expression is bound to context unconstrainedListElementType = unconstrainedListType.rootTypeConsApp().getArg(0); //first build a field map containing all the fields from the context final Map<FieldName, TypeExpr> fieldMap; //map of all the fields in the context RecordFieldName -> TypeExpr if (unconstrainedListElementType instanceof RecordType) { fieldMap = ((RecordType) unconstrainedListElementType).getHasFieldsMap(); } else { fieldMap = new HashMap<FieldName, TypeExpr>(); } //add all the additional fields from the editor if (firstChild.getTypeExpr() instanceof RecordType ) { RecordType elementType = (RecordType ) firstChild.getTypeExpr(); // unconstrainedListElementType; for (int i = 0; i < elementType.getNHasFields(); i++) { final FieldName fieldName = elementType.getHasFieldNames().get(i); if (!fieldMap.containsKey(fieldName)) { fieldMap.put(fieldName, TypeExpr.makeParametricType()); } } } //make sure the element type is actually supposed to be a record - otherwise just use generic type if (!(firstChild.getTypeExpr() instanceof RecordType) && ! (unconstrainedListElementType instanceof RecordType)) { unconstrainedListElementType = TypeExpr.makeParametricType(); } else { unconstrainedListElementType = TypeExpr.makeNonPolymorphicRecordType(fieldMap); } unconstrainedListType = valueEditorManager.getValueNodeBuilderHelper().getPreludeTypeConstants().makeListType(unconstrainedListElementType); } // Populate the map // List returnMap.put(listValueNode, unconstrainedListType); // List items for (final ValueNode listElementNode : listElementNodes) {//.iterator(); it.hasNext(); ) { returnMap.put(listElementNode, unconstrainedListElementType); if (!consolidateColumns) { // For tuple and record lists, we have to also put the individual elements of the list items if (isListOfNTupleVNs) { NTupleValueNode currentTupleValue = (NTupleValueNode)listElementNode; RecordType unconstrainedListElementRecordType = (RecordType)unconstrainedListElementType; Map<FieldName, TypeExpr> hasFieldsMap = unconstrainedListElementRecordType.getHasFieldsMap(); int j = 0; for (final TypeExpr unconstrainedTupleElementType : hasFieldsMap.values()) { ValueNode currentTupleItem = currentTupleValue.getValueAt(j); returnMap.put(currentTupleItem, unconstrainedTupleElementType); ++j; } } else if (isListRecord) { List<FieldName> contextRecordFieldNames = ((RecordType)unconstrainedListElementType).getHasFieldNames(); RecordValueNode currentRecordValue = (RecordValueNode)listElementNode; for (int j = 0, n = currentRecordValue.getNFieldNames(); j < n; j++) { ValueNode currentFieldItem = currentRecordValue.getValueAt(j); TypeExpr unconstrainedFieldType; if (contextRecordFieldNames.contains(currentRecordValue.getFieldName(j))) { unconstrainedFieldType = ((RecordType)unconstrainedListElementType).getHasFieldType(currentRecordValue.getFieldName(j)); } else { // This field does not exist in the context; then it was added to our list type // while the node was bound. // TODO: This causes failure during type switching; one suggestion is to "grow" // the context record here by adding this field name associated to a parametric type. unconstrainedFieldType = TypeExpr.makeParametricType(); } returnMap.put(currentFieldItem, unconstrainedFieldType); } } } } return returnMap; } private static final long serialVersionUID = 8120550072675651096L; /** * {@inheritDoc} */ @Override public void commitChildChanges(ValueNode oldChild, ValueNode newChild) { // Get the copy of the current value node, type switched if necessary. ListValueNode oldValueNode = (ListValueNode)getValueNode(); ListValueNode newValueNode; if (!oldChild.getTypeExpr().sameType(newChild.getTypeExpr())) { Map<ValueNode, TypeExpr> valueNodeToUnconstrainedTypeMap = getValueNodeToUnconstrainedTypeMap(); Map<ValueNode, ValueNode> commitValueMap = valueEditorManager.getValueNodeCommitHelper().getCommitValues(oldChild, newChild, valueNodeToUnconstrainedTypeMap); PreludeTypeConstants typeConstants = valueEditorManager.getPreludeTypeConstants(); TypeExpr charType = typeConstants.getCharType(); ValueNode newValueNodeFromMap = commitValueMap.get(oldValueNode); // HACK: if it's a ListOfCharValueNode, convert it to a ListValueNode. // What we need is a way to guarantee the type of value node that is returned by getCommitValues(). // This is not possible with the current form of transmuteValueNode() though. if (newValueNodeFromMap instanceof ListOfCharValueNode) { ListOfCharValueNode charListValueNode = (ListOfCharValueNode)newValueNodeFromMap; char[] charListValueArray = charListValueNode.getStringValue().toCharArray(); ArrayList<ValueNode> newListValue = new ArrayList<ValueNode>(charListValueArray.length); for (final char charListValue : charListValueArray) { newListValue.add(new LiteralValueNode(Character.valueOf(charListValue), charType)); } newValueNode = new ListValueNode(newListValue, typeConstants.getCharListType(), new LiteralValueNode(new Character('a'), charType)); replaceValueNode(newValueNode, true); return; } else { newValueNode = (ListValueNode)newValueNodeFromMap; } } else { newValueNode = (ListValueNode)oldValueNode.copyValueNode(); } // Modify the new value node so that the old child is replaced by the new child. // Note that the cell editor may now be editing a different row so we have to search for the row that changed // This can happen if one clicks from an editor for one cell to another cell // (eg. in a list of colours, from a colour value editor for one cell, onto the cell editor for another cell.) List<ValueNode> oldChildrenList = oldValueNode.getValue(); List<ValueNode> newChildrenList = newValueNode.getValue(); boolean isListRecord = getListElementType().rootRecordType() != null; if (consolidateColumns || !isListRecord) { for (int i = 0, listSize = newValueNode.getNElements(); i < listSize; i++) { if (oldChildrenList.get(i) == oldChild) { TypeExpr childType = (newChildrenList.get(i)).getTypeExpr(); newValueNode.setValueNodeAt(i, newChild.copyValueNode(childType)); break; } } } else { // isListRecord == true, ie. must be a list of records (or tuples). AbstractRecordValueNode firstChild = (AbstractRecordValueNode)oldChildrenList.get(0); // To find the child that changed, for each value in the list, have to iterate through the tuple/record. int listSize = newValueNode.getNElements(); int childrenSize = firstChild.getNFieldNames(); // TODO: Note that new record value nodes may have more/less fields than the old nodes, // because of type switch listItem: for (int i = 0; i < listSize; i++) { AbstractRecordValueNode currentValue = (AbstractRecordValueNode)oldChildrenList.get(i); for (int j = 0; j < childrenSize; j++) { if (currentValue.getValueAt(j) == oldChild) { AbstractRecordValueNode newListItemValue = (AbstractRecordValueNode)newChildrenList.get(i); newListItemValue.setValueNodeAt(j, newChild.copyValueNode()); break listItem; } } } } // Set the value node replaceValueNode(newValueNode, true); // Update UI userHasResized(); } /** * @return the action for switching consolidating of columns */ protected Action getSwitchConsolidationAction() { if (switchConsolidationAction == null) { switchConsolidationAction = new AbstractAction("SwitchConsolidation") { private static final long serialVersionUID = -2706186354624718070L; public void actionPerformed(ActionEvent evt) { // Perform switch consolidateColumns = !consolidateColumns; ((ListTupleTableModel)table.getModel()).setConsolidatingColumns(consolidateColumns); // Update UI updateColumnSizes(); initializeTableCellRenderers(); enableButtonsForTableState(); } }; switchConsolidationAction.putValue(Action.SHORT_DESCRIPTION, ValueEditorMessages.getString("VE_ColumnConsolidation")); } return switchConsolidationAction; } /** * @return the action for adding a new record field */ protected Action getAddFieldAction() { if (addFieldAction == null) { addFieldAction = new AbstractAction("AddField") { private static final long serialVersionUID = -4498524901433254375L; public void actionPerformed(ActionEvent evt) { boolean isBasePolymorphicRecord = !isContextNonRecordPolymorphic(); if (!isBasePolymorphicRecord) { throw new IllegalStateException("Cannot add field to non-polymorphic record"); } // Find a name for the field, which does not conflict with the existing has or lacks fields List<FieldName> hasFields = getListElementType().rootRecordType().getHasFieldNames(); Set<FieldName> lacksFields = getListElementType().rootRecordType().getLacksFieldsSet(); FieldName newFieldName = RecordValueEditor.getNewFieldName(hasFields, lacksFields); // Create new record type Map<FieldName, TypeExpr> fieldNamesToTypeMap = new HashMap<FieldName, TypeExpr>(); for (final FieldName hasFieldName : hasFields) { TypeExpr hasFieldType = getListElementType().rootRecordType().getHasFieldType(hasFieldName); fieldNamesToTypeMap.put(hasFieldName, hasFieldType); } fieldNamesToTypeMap.put(newFieldName, TypeExpr.makeParametricType()); RecordType newRecordType = TypeExpr.makeNonPolymorphicRecordType(fieldNamesToTypeMap); ValueNodeBuilderHelper valueNodeBuilderHelper = valueEditorManager.getValueNodeBuilderHelper(); replaceValueNode(getValueNode().transmuteValueNode(valueNodeBuilderHelper, valueEditorManager.getValueNodeTransformer(), valueNodeBuilderHelper.getPreludeTypeConstants().makeListType(newRecordType)), true); // Update column sizes after switch userHasResized(); // Select the new field int fieldIndex = getListElementType().rootRecordType().getHasFieldNames().indexOf(newFieldName); if (getRowCount() > 0) { selectCell(0, fieldIndex); } } }; addFieldAction.putValue(Action.SHORT_DESCRIPTION, ValueEditorMessages.getString("VE_AddRecordField")); } return addFieldAction; } /** * @return the action of removing a record field */ protected Action getDeleteFieldAction() { if (deleteFieldAction == null) { deleteFieldAction = new AbstractAction("DeleteField") { private static final long serialVersionUID = -1055163203967443486L; public void actionPerformed(ActionEvent evt) { boolean isBasePolymorphicRecord = !isContextNonRecordPolymorphic(); if (!isBasePolymorphicRecord) { throw new IllegalStateException("Cannot add field to non-polymorphic record"); } int col = getSelectedColumn(); if (col == -1) { return; } List<FieldName> existingFields = getListElementType().rootRecordType().getHasFieldNames(); FieldName fieldName = existingFields.get(col); // Create new record type lacking the field Map<FieldName, TypeExpr> fieldNamesToTypeMap = new HashMap<FieldName, TypeExpr>(); if (!existingFields.remove(fieldName)) { throw new IllegalStateException(); } for (final FieldName existingFieldName : existingFields) { TypeExpr existingFieldType = getListElementType().rootRecordType().getHasFieldType(existingFieldName); fieldNamesToTypeMap.put(existingFieldName, existingFieldType); } RecordType newRecordType = TypeExpr.makeNonPolymorphicRecordType(fieldNamesToTypeMap); ValueNodeBuilderHelper valueNodeBuilderHelper = valueEditorManager.getValueNodeBuilderHelper(); replaceValueNode(getValueNode().transmuteValueNode(valueNodeBuilderHelper, valueEditorManager.getValueNodeTransformer(), valueNodeBuilderHelper.getPreludeTypeConstants().makeListType(newRecordType)), true); userHasResized(); // Handle selection int colCount = getColumnCount(); if (colCount != 0) { int newSelectedCol = 0; // Update the highlighted/selected column and edited cell. if (colCount == col) { // Move highlight/selection left one column, since we just deleted the last column. newSelectedCol = col - 1; } else { // Keep the selection in its old column. newSelectedCol = col; } selectCell(getSelectedRow(), newSelectedCol); } } }; deleteFieldAction.putValue(Action.SHORT_DESCRIPTION, "Remove Record Field"); } return deleteFieldAction; } /** * @return the button for adding a record field */ protected JButton getAddFieldButton() { if (addFieldButton == null) { addFieldButton = new JButton(getAddFieldAction()); addFieldButton.setName("AddFieldButton"); addFieldButton.setText(""); addFieldButton.setIcon(addColumnIcon); addFieldButton.setMargin(new Insets(0, 0, 0, 0)); } return addFieldButton; } /** * @return the button for removing a record field */ protected JButton getDeleteFieldButton() { if (deleteFieldButton == null) { deleteFieldButton = new JButton(getDeleteFieldAction()); deleteFieldButton.setName("DeleteFieldButton"); deleteFieldButton.setText(""); deleteFieldButton.setIcon(deleteColumnIcon); deleteFieldButton.setMargin(new Insets(0, 0, 0, 0)); } return deleteFieldButton; } /** * @return the button for consolidating columns */ protected JButton getConsolidateColumnsButton() { if (consolidateColumnsButton == null) { consolidateColumnsButton = new JButton(getSwitchConsolidationAction()); consolidateColumnsButton.setName("ConsolidateColumnsButton"); consolidateColumnsButton.setMnemonic('u'); consolidateColumnsButton.setText(""); consolidateColumnsButton.setIcon(consolidateColumnsIcon); consolidateColumnsButton.setMargin(new Insets(0, 0, 0, 0)); } return consolidateColumnsButton; } /** * @return a new panel for the east side of the editor, containing arrows and field addition buttons */ @Override protected JPanel createEastPanel() { JPanel ivjEastPanel = new JPanel(); ivjEastPanel.setName("EastPanel"); ivjEastPanel.setBorder(new EtchedBorder()); ivjEastPanel.setLayout(new FlowLayout()); JPanel ivjArrowPanel = new JPanel(); ivjArrowPanel.setName("ArrowPanel"); ivjArrowPanel.setLayout(new BoxLayout(ivjArrowPanel, BoxLayout.Y_AXIS)); ivjArrowPanel.setLayout(new GridBagLayout()); { GridBagConstraints constraints = new GridBagConstraints(); constraints.gridx = 0; constraints.gridy = 0; constraints.gridwidth = 1; constraints.weightx = 0.0; constraints.weighty = 0.0; constraints.insets = new Insets(0,0,2,0); constraints.fill = GridBagConstraints.HORIZONTAL; ivjArrowPanel.add(getAddFieldButton(), constraints); } { GridBagConstraints constraints = new GridBagConstraints(); constraints.gridx = 0; constraints.gridy = 1; constraints.gridwidth = 1; constraints.weightx = 0.0; constraints.weighty = 0.0; constraints.insets = new Insets(2,0,10,0); constraints.fill = GridBagConstraints.HORIZONTAL; ivjArrowPanel.add(getDeleteFieldButton(), constraints); } { GridBagConstraints constraints = new GridBagConstraints(); constraints.gridx = 0; constraints.gridy = 2; constraints.gridwidth = 1; constraints.weightx = 0.0; constraints.weighty = 0.0; constraints.insets = new Insets(2,0,2,0); constraints.fill = GridBagConstraints.HORIZONTAL; ivjArrowPanel.add(getUpButton(), constraints); } { GridBagConstraints constraints = new GridBagConstraints(); constraints.gridx = 0; constraints.gridy = 3; constraints.gridwidth = 1; constraints.weightx = 0.0; constraints.weighty = 0.0; constraints.insets = new Insets(2,0,2,0); constraints.fill = GridBagConstraints.HORIZONTAL; ivjArrowPanel.add(getDownButton(), constraints); } { GridBagConstraints constraints = new GridBagConstraints(); constraints.gridx = 0; constraints.gridy = 4; constraints.gridwidth = 1; constraints.weightx = 0.0; constraints.weighty = 0.0; constraints.insets = new Insets(10,0,10,0); constraints.fill = GridBagConstraints.HORIZONTAL; ivjArrowPanel.add(getConsolidateColumnsButton(), constraints); } ivjEastPanel.add(ivjArrowPanel, ivjArrowPanel.getName()); return ivjEastPanel; } /** * {@inheritDoc} */ @Override protected void initializeTableCellRenderers() { // Initialize table header and cell renderers updateTableHeader(); super.initializeTableCellRenderers(); // And reset column header renderers (these are set to default renderer by initializeTableCellRenderers) if (!consolidateColumns && getListElementType().rootRecordType() != null) { if (getListElementType().rootRecordType().getNHasFields() == 0) { TableCellRenderer emptyCellRenderer = new EmptyCellRenderer(); TableColumnModel columnModel = table.getColumnModel(); for (int i = 0, n = columnModel.getColumnCount(); i < n; i++ ) { TableColumn column = columnModel.getColumn(i); column.setCellRenderer(emptyCellRenderer); } } else { editableRecordHeader.setColumnModel(table.getColumnModel()); } } } /** * Overwrites method to enable/disable buttons for record field modification. * @see org.openquark.gems.client.valueentry.AbstractListValueEditor#enableButtonsForTableState() */ @Override protected void enableButtonsForTableState() { super.enableButtonsForTableState(); boolean isRecordList = getListElementType().rootRecordType() != null; boolean isTupleList = getListElementType().isTupleType(); getAddFieldButton().setVisible(isRecordList); getDeleteFieldButton().setVisible(isRecordList); if (isRecordList) { getAddFieldAction().setEnabled(!consolidateColumns); getDeleteFieldAction().setEnabled(!consolidateColumns); } getConsolidateColumnsButton().setVisible(isRecordList || isTupleList); if (!consolidateColumns && getListElementType().rootRecordType() != null) { enableRecordFieldAddRemoveButtons(); } } /** Enables or disables the remove button if a field can be edited */ protected void enableRecordFieldAddRemoveButtons() { if (getListElementType().rootRecordType() == null) { throw new IllegalStateException("Attempting to enable column add/remove buttons for non-record type"); } boolean isBasePolymorphicRecord = !isContextNonRecordPolymorphic(); getDeleteFieldAction().setEnabled(isBasePolymorphicRecord); getAddFieldAction().setEnabled(isBasePolymorphicRecord); int selectedFieldIndex = getSelectedColumn(); if ( getListElementType().rootRecordType().getNHasFields() != 0 && selectedFieldIndex != -1 && selectedFieldIndex < getColumnCount() && ((RecordValueEditor.EditableHeader)table.getTableHeader()).isCellEditable(selectedFieldIndex)) { getDeleteFieldAction().setEnabled(true); } else { getDeleteFieldAction().setEnabled(false); } } /** * @return whether the context is a list of non-record-polymoprhic records. * * Ex: if the context is [(r\a) => {r | age::a}], returns False * Ex: if the context is [a], returns True */ private boolean isContextNonRecordPolymorphic() { TypeExpr leastConstrainedContextType = getContext().getLeastConstrainedTypeExpr(); return (leastConstrainedContextType.rootTypeConsApp() != null && leastConstrainedContextType.rootTypeConsApp().getArg(0).rootRecordType() != null && !leastConstrainedContextType.rootTypeConsApp().getArg(0).rootRecordType().isRecordPolymorphic()); } /** * @return index of the field currently selected (either through the table or header) */ @Override protected int getSelectedColumn() { int selectedIndex = super.getSelectedColumn(); if (selectedIndex == -1) { // No cell selected; the column header editor may be selected JTableHeader tableHeader = table.getTableHeader(); if (tableHeader instanceof RecordValueEditor.EditableHeader) { selectedIndex = ((RecordValueEditor.EditableHeader)tableHeader).getSelectedColumn(); } } return selectedIndex; } /** * Performs updates to the table header, enabling editing only for lists of records. */ protected void updateTableHeader() { JTableHeader newHeader; if (!consolidateColumns && getListElementType().rootRecordType() != null && getListElementType().rootRecordType().getNHasFields() > 0) { // Table is modeling a list of records, with non-consolidated columns which should have // editable headers TypeExpr leastConstrainedType = getContext().getLeastConstrainedTypeExpr(); if (leastConstrainedType != null && leastConstrainedType.rootTypeConsApp() != null) { leastConstrainedType = leastConstrainedType.rootTypeConsApp().getArg(0).rootRecordType(); } editableRecordHeader.getRecordFieldModel().initialize(((TypeConsApp)getValueNode().getTypeExpr()).getArg(0).rootRecordType(), leastConstrainedType); newHeader = editableRecordHeader; } else { // Table header should be non editable newHeader = new JTableHeader(table.getColumnModel()); newHeader.setReorderingAllowed(false); } table.setTableHeader(newHeader); tableScrollPane.setColumnHeaderView(newHeader); newHeader.repaint(); if (dragPointHandler != null) { DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer( newHeader, DnDConstants.ACTION_COPY_OR_MOVE, new ListTupleDragGestureListener()); } } /** * Perform UI updates necessary when setting initial value. * * Note: If the list elements are records which are non-record-polymorphic * then this method ensures that the type becomes record-polymorphic if the context allows. * Ex: If the list type is [{age::Double}] and context is [a], the type is transmuted to (r\age)=>[{r|age::Double}] * * @see org.openquark.gems.client.valueentry.ValueEditor#setInitialValue() */ @Override public void setInitialValue() { super.setInitialValue(); if (getListElementType().rootRecordType() != null) { // If we have a list of records in an unconstrained context, make sure the list element types // are not record polymorphic // This is an allowed operation because we are actually specializing the list element type. // TODO: Enforce non-record-polymorphic regardless of context. // The CAL value produced by the value node is always non-record-polymorphic, thus the // type of the list value node should be consistent with this. // // This is currently not possible because the type switching mechanism does not // properly update a record type switch form record-polymorphic to non-record-polymorphic. TypeExpr unconstrainedListType = getContext().getLeastConstrainedTypeExpr(); if (unconstrainedListType.rootTypeVar() != null || unconstrainedListType.rootTypeConsApp().getArg(0).rootTypeVar() != null) { // Not constrained by context, so create new type expression according to the fields we have if (getListElementType().rootRecordType().isRecordPolymorphic()) { Map<FieldName, TypeExpr> fieldNamesToTypeMap = new HashMap<FieldName, TypeExpr>(); List<FieldName> existingFields = getListElementType().rootRecordType().getHasFieldNames(); for (final FieldName existingFieldName : existingFields) { TypeExpr existingFieldType = getListElementType().rootRecordType().getHasFieldType(existingFieldName); fieldNamesToTypeMap.put(existingFieldName, existingFieldType); } RecordType newRecordType = TypeExpr.makeNonPolymorphicRecordType(fieldNamesToTypeMap); ValueNodeBuilderHelper valueNodeBuilderHelper = valueEditorManager.getValueNodeBuilderHelper(); replaceValueNode(getValueNode().transmuteValueNode(valueNodeBuilderHelper, valueEditorManager.getValueNodeTransformer(), valueNodeBuilderHelper.getPreludeTypeConstants().makeListType(newRecordType)), true); notifyValueChanged(getValueNode()); } } } updateTableHeader(); enableButtonsForTableState(); if (!initializedOnce) { initializedOnce = true; } } /** * Overwrite loadSavedSize() to resize columns only if the editor has not been * previously initialized. * * @see org.openquark.gems.client.valueentry.TableValueEditor#loadSavedSize(org.openquark.gems.client.valueentry.ValueEditor.Info) */ @Override protected void loadSavedSize(ValueEditor.Info sizeInfo) { if (initializedOnce) { return; } super.loadSavedSize(sizeInfo); } }