/*
* 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.
*/
/*
* RecordValueEditor.java
* Created: Jul 12, 2004
* By: Iulian Radu
*/
package org.openquark.gems.client.valueentry;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragSource;
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.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
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.DefaultCellEditor;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JViewport;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.BevelBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.MouseInputListener;
import javax.swing.plaf.basic.BasicTableHeaderUI;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
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.RecordType;
import org.openquark.cal.compiler.TypeExpr;
import org.openquark.cal.valuenode.AbstractRecordValueNode;
import org.openquark.cal.valuenode.ValueNode;
import org.openquark.cal.valuenode.ValueNodeBuilderHelper;
import org.openquark.gems.client.ToolTipHelpers;
import org.openquark.util.UnsafeCast;
import org.openquark.util.ui.UIUtilities;
/**
* The ValueEditor for Record Value Nodes.
*
* This editor displays record fields tabular format, and allows editing of field values through
* individual value entry panels. For non-record-polymorphic records, this editor also allows
* addition and removal of record fields. Depending on the value editor context, field names
* may also be renamed.
*
* @author Iulian Radu
*/
public final class RecordValueEditor extends TableValueEditor {
/**
* This interface defines what drag operations are supported by the
* <code>RecordValueEditor</code>. Implementors of this interface will be
* called when a certain drag operation is initiated by the user.
*/
public interface RecordValueDragPointHandler extends ValueEditorDragPointHandler {
/**
* This method defines the behaviour when the user attempts to drag a portion
* of a tuple from the <code>RecordValueEditor</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 fieldElementIndex
* @return boolean
*/
boolean dragFieldItem(DragGestureEvent dge, RecordValueEditor parentEditor, int fieldElementIndex);
}
/**
* A custom value editor provider for the RecordValueEditor.
*/
public static class RecordValueEditorProvider extends ValueEditorProvider<RecordValueEditor> {
public RecordValueEditorProvider(ValueEditorManager valueEditorManager) {
super(valueEditorManager);
}
/**
* {@inheritDoc}
*/
@Override
public boolean canHandleValue(ValueNode valueNode, SupportInfo providerSupportInfo) {
return (valueNode instanceof AbstractRecordValueNode) &&
(hasSupportedFieldTypes((AbstractRecordValueNode)valueNode, providerSupportInfo));
}
/**
* {@inheritDoc}
*/
@Override
public RecordValueEditor getEditorInstance(ValueEditorHierarchyManager valueEditorHierarchyManager,
ValueNode valueNode) {
RecordValueEditor editor = new RecordValueEditor(valueEditorHierarchyManager, null);
editor.setOwnerValueNode(valueNode);
return editor;
}
/**
* {@inheritDoc}
*/
@Override
public RecordValueEditor getEditorInstance(ValueEditorHierarchyManager valueEditorHierarchyManager,
ValueNodeBuilderHelper valueNodeBuilderHelper,
ValueEditorDragManager dragManager,
ValueNode valueNode) {
RecordValueEditor editor = new RecordValueEditor(valueEditorHierarchyManager,
getListFieldDragPointHandler(dragManager));
editor.setOwnerValueNode(valueNode);
return editor;
}
/**
* {@inheritDoc}
*/
@Override
public boolean usableForOutput() {
return true;
}
/**
* Checks if the value nodes for the field elements are supported by value editors.
* @param valueNode the field value node to check for
* @return true if all element value nodes are supported
*/
private boolean hasSupportedFieldTypes(AbstractRecordValueNode valueNode, SupportInfo providerSupportInfo) {
// Notify the info object that the value node's type is supported..
providerSupportInfo.markSupported(valueNode.getTypeExpr());
ValueEditorManager valueEditorManager = getValueEditorManager();
for (int i = 0, size = valueNode.getTypeExpr().rootRecordType().getNHasFields(); i < size; i++) {
ValueNode elementValueNode = valueNode.getValueAt(i);
if (!valueEditorManager.isSupportedValueNode(elementValueNode, providerSupportInfo)) {
return false;
}
}
return true;
}
/**
* A convenient method for casting the drag point handler to the type that is
* suitable for the <code>RecordValueEditor</code> to use. If such conversion is not
* possible, then this method should return <code>null</code>.
*
* @param dragManager
* @return FieldDragPointHandler
*/
private RecordValueDragPointHandler getListFieldDragPointHandler(ValueEditorDragManager dragManager) {
ValueEditorDragPointHandler handler = getDragPointHandler(dragManager);
if (handler instanceof RecordValueDragPointHandler) {
return (RecordValueDragPointHandler) handler;
}
return null;
}
}
/**
* A record field model holds information about the value editor record type and its context.
* Specifically, this holds the names of 'has' and 'lacks' fields in the editor record and
* its context.
*
* Ex: If we are editing the record (r\age,r\name) => {r|age::Double, name::String} with age=1.0, name="Roger",
* within the context (r\age) => {r|age::a}, the model would hold the following information:
* contextFieldNames = age
* hasFieldNames = age, name
* lacksFieldNames = age, name
*
* A single such object is intended to model the editor current record type, providing update and
* query services for various UI components.
*
* @author Iulian Radu
*/
static class RecordFieldModel {
/**
* Interface used to inform holders of record field models that some change
* has occurred in the record fields.
* @author Iulian Radu
*/
interface ModelChangeListener {
/**
* Respond to a change in the field model.
*/
public void recordFieldModelChanged();
}
/** List of listeners to be informed of changes to this model */
private final List<ModelChangeListener> changeListeners;
/** List of has field names (sorted in the order of the RecordFieldName class) */
private final List<FieldName> hasFieldNames;
/** Set of lacks field names */
private final Set<FieldName> lacksFieldNames;
/** Set of has and lacks field names from the context.
* These are grouped together because value editors may not edit or overwrite them. */
private final Set<FieldName> contextFieldNames;
/** Constructor **/
RecordFieldModel() {
hasFieldNames = new ArrayList<FieldName>();
lacksFieldNames = new HashSet<FieldName>();
contextFieldNames = new HashSet<FieldName>();
changeListeners = new ArrayList<ModelChangeListener>();
}
/** Adds the specified change listener to this model */
public void addModelChangeListener(ModelChangeListener listener) {
changeListeners.add(listener);
}
/** Removes the specified change listener from this model */
public void removeModelChangeListener(ModelChangeListener listener) {
changeListeners.remove(listener);
}
/** Notifies the listeners that this model has changed */
private void notifyModelChanged() {
for (final ModelChangeListener changeListener : changeListeners) {
changeListener.recordFieldModelChanged();
}
}
/**
* Builds the record field information from the type of the valuenode and its context.
* If the fields acquired differ from the previously recorded fields, the listeners are informed
* that the model has changed.
* @param recordTypeExpr type expression of the ValueNode record
* @param contextRecordTypeExpr type expression of the ValueNode record context
*/
public void initialize(RecordType recordTypeExpr, TypeExpr contextRecordTypeExpr) {
// Get names of all fields
List<FieldName> hasFieldNames = recordTypeExpr.getHasFieldNames();
Set<FieldName> lacksFieldNames = recordTypeExpr.getLacksFieldsSet();
// Gather the fields from the context
Set<FieldName> contextFieldNames = new HashSet<FieldName>();
if (contextRecordTypeExpr != null && contextRecordTypeExpr.rootRecordType() != null) {
contextFieldNames.addAll(contextRecordTypeExpr.rootRecordType().getHasFieldNames());
contextFieldNames.addAll(contextRecordTypeExpr.rootRecordType().getLacksFieldsSet());
}
// If necessary, perform updates to the model and notify listeners
if ( !hasFieldNames.equals(this.hasFieldNames) ||
!lacksFieldNames.equals(this.lacksFieldNames) ||
!contextFieldNames.equals(this.contextFieldNames)) {
this.hasFieldNames.clear();
this.hasFieldNames.addAll(hasFieldNames);
this.lacksFieldNames.clear();
this.lacksFieldNames.addAll(lacksFieldNames);
this.contextFieldNames.clear();
this.contextFieldNames.addAll(contextFieldNames);
notifyModelChanged();
}
}
/**
* Get the name of a record 'has field', specifying its index in the ordering of the RecordFieldName class.
*
* @param index field name index
* @return name of the specified field
*/
public FieldName getHasFieldName(int index) {
return hasFieldNames.get(index);
}
/**
* @return the number of has fields in this record
*/
public int getNHasFields() {
return hasFieldNames.size();
}
/**
* @param fieldName
* @return whether the field is specified by the context (either as a has field or lacks field)
*/
public boolean isContextField(FieldName fieldName) {
return contextFieldNames.contains(fieldName);
}
/**
* @return whether the field is specified as a 'has field' of the record
*/
public boolean isHasField(FieldName fieldName) {
return hasFieldNames.contains(fieldName);
}
/**
* @return whether the field is specified in the record lacks fields
*/
public boolean isLacksField(FieldName fieldName) {
return lacksFieldNames.contains(fieldName);
}
}
/**
* A simple one-column table whose cells display the fields of the value editor record.
*
* The table cells indicate field name, type and editability. Fields names not bound by the
* editor context are considered renameable, and thus specialized text editors are provided
* for these cells.
*
* A record field model, representing the editor record type, is monitored by this object.
*
* @author Iulian Radu
*/
static class RowHeader extends JTable implements RecordFieldModel.ModelChangeListener {
private static final long serialVersionUID = -4067610194584025529L;
/**
* Interface used by the header to communicate with its owner when a field
* is renamed, or when type information is required.
*
* @author Iulian Radu
*/
public interface RowHeaderOwner {
/**
* Retrieve the type of the record field at the specified (RecordFieldName ordered) index.
* @return field type expression
*/
public TypeExpr getFieldType(int index);
/**
* Perform actions necessary to rename the record field oldName to the newName
*
* @param oldName original name of the field
* @param newName new name of the field
*/
public void renameField(FieldName oldName, FieldName newName);
}
/**
* Cell renderer for the record field name cells.
* These cells display field name, type icon, and editability.
*
* @author Iulian Radu
*/
static class CellRenderer extends DefaultTableCellRenderer {
private static final long serialVersionUID = 2226106228605244181L;
/** Editor which this cell renderer belongs to (this is used to determine cell editability and layout) */
private final ValueEditor recordEditor;
/** Header owner of the renderer */
private final RowHeaderOwner headerOwner;
/** Whether the fields are laid out horizontally.
* If true, record fields are displayed on individual rows; otherwise, they are
* displayed in individual columns */
private final boolean horizontalLayout;
/** The record field model representing the rendered fields */
private final RecordFieldModel fieldModel;
/**
* Constructor
*
* @param recordEditor value editor owning this cell renderer
* @param horizontalLayout whether the fields are laid out horizontally (if true, fields are displayed on individual rows)
* @param headerOwner owner of the row header, to be queried about field type
* @param fieldModel field model, queried about field editability
*/
public CellRenderer (ValueEditor recordEditor, boolean horizontalLayout, RowHeaderOwner headerOwner, RecordFieldModel fieldModel) {
if ((recordEditor == null) || (headerOwner == null)) {
throw new NullPointerException();
}
this.headerOwner = headerOwner;
this.recordEditor = recordEditor;
this.horizontalLayout = horizontalLayout;
this.fieldModel = fieldModel;
}
/**
* Retrieves a label used for rendering a field name and its type icon.
* The label will have a dark gray background if the field is not editable.
* @see javax.swing.table.TableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
*/
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
if (table != null && table.getTableHeader() != null) {
JTableHeader header = table.getTableHeader();
setForeground(header.getForeground());
setBackground(header.getBackground());
setFont(header.getFont());
} else {
setForeground(UIManager.getColor("TableHeader.foreground"));
setBackground(UIManager.getColor("TableHeader.background"));
setFont(UIManager.getFont("TableHeader.font"));
}
String displayText = (value == null) ? "" : value.toString();
setText(displayText);
boolean isNotEditable = fieldModel.isContextField(fieldModel.getHasFieldName(horizontalLayout? row : column)) && recordEditor.isEditable();
if (isNotEditable) {
setForeground(Color.GRAY);
setBorder(new BevelBorder(BevelBorder.RAISED, Color.WHITE, Color.LIGHT_GRAY, Color.GRAY, Color.LIGHT_GRAY));
} else {
Color background = getBackground();
setBackground(UIUtilities.brightenColor(background, 0.85));
setBorder(new BevelBorder(BevelBorder.RAISED, Color.WHITE, getBackground(), Color.GRAY, getBackground()));
}
setHorizontalAlignment(SwingConstants.LEFT);
TypeExpr typeExpr = headerOwner.getFieldType(horizontalLayout ? row : column);
String iconName = recordEditor.valueEditorManager.getTypeIconName(typeExpr);
ImageIcon typeIcon = new ImageIcon(getClass().getResource(iconName));
setIcon(typeIcon);
String typeExprTip = typeExpr.toString();
typeExprTip = ToolTipHelpers.wrapTextToHTMLLines(typeExprTip, this);
String tooltip = "<html><body><b>" + displayText + "</b> :: <i>" + typeExprTip + "</i>";
if (isNotEditable) {
tooltip += ValueEditorMessages.getString("VE_ContextName");
}
tooltip += "</body></html>";
setToolTipText(tooltip);
return this;
}
}
/**
* Table model for the RowHeader table.
* This maps the field names from the record model to a table model, and properly indicates
* editability of fields.
*
* @author Iulian Radu
*/
private static class HeaderTableModel extends DefaultTableModel {
private static final long serialVersionUID = -3290644335698498773L;
/** If true, the layout is horizontal and the fields are stored in rows; else, fields are in columns*/
private final boolean horizontalLayout;
/** Record field model which this header represents */
private final RecordFieldModel fieldModel;
/** Constructor */
HeaderTableModel(boolean horizontalLayout, RecordFieldModel fieldModel) {
super();
this.horizontalLayout = horizontalLayout;
this.fieldModel = fieldModel;
}
/** Queries the field model and indicates whether a field can be edited */
@Override
public boolean isCellEditable(int row, int col) {
return !fieldModel.isContextField(fieldModel.getHasFieldName(horizontalLayout? row : col));
}
}
/**
* Cell editor for record field names; uses a specialized text field which changes
* color if the record cannot contain a field with the specified value.
*
* Though the same editor is used for all table cells, the editor uses only one text
* field as input component, and thus assumes that only one of its cells is edited at
* a time.
*
* @author Iulian Radu
*/
static class FieldNameCellEditor extends DefaultCellEditor {
/**
* Text field for editing cells identifying field names.
* If entering an invalid field name, this field changes its color and updates
* its tooltip accordingly.
*
* @author Iulian Radu
*/
private static class CellEditorTextField extends JTextField {
private static final long serialVersionUID = 5681713375183368462L;
/** Model holding information about the record fields */
private final RecordFieldModel recordFieldModel;
/** Whether the name in this component is proper for a record field */
private boolean properFieldName = true;
/** Stores a copy of the original field name which is edited (ie: is set proper to editing start) */
private String editedFieldName = "";
/** Stores the number of the row being edited */
private int editedRow = -1;
/** Stores the number of the column being edited */
private int editedColumn = -1;
/** If true, the layout is horizontal and the fields are displayed on individual rows;
* else, fields are displayed in individual columns*/
private final boolean horizontalLayout;
/** Whether this text field is currently used for editing */
private boolean isEditing = false;
/**
* Listener for changes in the caret of this text component. Depending on the text
* typed, this modifies the text color and tooltip to indicate field name validity.
*
* This type of listener is used because of difficulties with key listeners in table cell editors.
*/
private final CaretListener caretListener = new CaretListener() {
public void caretUpdate(CaretEvent e) {
String myText = CellEditorTextField.this.getText();
FieldName myTextAsFieldName = FieldName.make(myText);
boolean validName = myTextAsFieldName != null;
boolean isExistingHasField = validName ? recordFieldModel.isHasField(myTextAsFieldName) : false;
boolean isContextLessField = validName ? recordFieldModel.isContextField(myTextAsFieldName) : false;
if (!editedFieldName.equals(myText) && (!validName || isContextLessField || isExistingHasField)) {
properFieldName = false;
setForeground(new Color(Color.GRAY.getRed(), Color.GRAY.getGreen(), Color.GRAY.getBlue(), Color.GRAY.getAlpha() - 50));
//todoBI these resources must be localized.
if (isExistingHasField) {
setToolTipText(myText + ValueEditorMessages.getString("VE_FieldAlreadyContained"));
} else if (isContextLessField) {
setToolTipText(myText + ValueEditorMessages.getString("VE_FieldMustBeExcluded"));
} else if (!validName) {
setToolTipText(myText + ValueEditorMessages.getString("VE_InvalidFieldName"));
}
} else {
setForeground(Color.BLACK);
setToolTipText(myText);
properFieldName = true;
}
}
};
/** Constructor */
CellEditorTextField(RecordFieldModel recordFieldModel, boolean horizontalLayout) {
this.recordFieldModel = recordFieldModel;
this.addCaretListener(caretListener);
this.horizontalLayout = horizontalLayout;
}
/** Prepare for starting an edit. This initializes the editing row/column and field name */
void initializeEdit(int row, int column) {
if (recordFieldModel == null) {
throw new IllegalStateException();
}
this.editedFieldName = recordFieldModel.getHasFieldName(horizontalLayout ? row : column).getCalSourceForm();
this.editedRow = row;
this.editedColumn = column;
this.isEditing = true;
setForeground(Color.BLACK);
}
/** Indicate that editing has stopped */
void finishedEdit() {
this.isEditing = false;
}
/** Indicates whether the field name is proper */
boolean isValidFieldName() {
return properFieldName;
}
/** @return the name of the edited field (ie: value prior to editing) */
public String getEditedFieldName() {
return editedFieldName;
}
/** @return the index of the row edited */
public int getEditedRow() {
return editedRow;
}
/** @return the index of the column edited */
public int getEditedColumn() {
return editedColumn;
}
/** @return whether this text field is performing editing */
public boolean isEditing() {
return isEditing;
}
}
/** Constructor */
FieldNameCellEditor(RecordFieldModel recordFieldModel, boolean horizontalLayout) {
super(new CellEditorTextField(recordFieldModel, horizontalLayout));
// Add listener for the field to stop editing on focus lost / escape key
this.getComponent().addFocusListener(new FocusListener() {
public void focusGained(FocusEvent e) {
}
public void focusLost(FocusEvent e) {
FieldNameCellEditor.this.stopCellEditing();
}
});
this.getComponent().addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_ESCAPE) {
FieldNameCellEditor.this.cancelCellEditing();
return;
}
}
});
}
private static final long serialVersionUID = -2392810011163295473L;
/** {@inheritDoc} */
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
((CellEditorTextField)editorComponent).initializeEdit(row, column);
return super.getTableCellEditorComponent(table, value, isSelected, row, column);
}
/**
* Adds the specified focus listener to the editor component
* @param listener
*/
public void addFocusListener(FocusListener listener) {
editorComponent.addFocusListener(listener);
}
}
/** Listener for field name editor cells */
private final CellEditorListener editorListener;
/** The VEP which contains this row header (used for UI layout)*/
private final ValueEditor recordEditor;
/** Header owner for this row header */
private final RowHeaderOwner headerOwner;
/** Model holding the record field names */
private final RecordFieldModel recordFieldModel;
/**
* Constructor
*
* @param recordEditor the value editor owning this row header
* @param table the table whose rows we are heading
* @param headerOwner owner of this header, responsible for performing renames
* @param recordFieldModel field model represented
*/
RowHeader(ValueEditor recordEditor, JTable table, RowHeaderOwner headerOwner, RecordFieldModel recordFieldModel) {
super();
setModel(new HeaderTableModel(true, recordFieldModel));
this.setRowHeight(table.getRowHeight());
((DefaultTableModel)this.getModel()).addColumn("fieldName");
TableCellRenderer cellRenderer = new CellRenderer(recordEditor, true, headerOwner, recordFieldModel);
TableCellEditor cellEditor = new FieldNameCellEditor(recordFieldModel, true);
((FieldNameCellEditor)cellEditor).setClickCountToStart(1);
this.getColumn("fieldName").setCellRenderer(cellRenderer);
this.getColumn("fieldName").setCellEditor(cellEditor);
this.recordEditor = recordEditor;
this.headerOwner = headerOwner;
this.recordFieldModel = recordFieldModel;
recordFieldModel.addModelChangeListener(this);
// Add listener to the editor to perform renaming on commit
editorListener = new CellEditorListener() {
public void editingCanceled(ChangeEvent e) {
FieldNameCellEditor.CellEditorTextField textField = (FieldNameCellEditor.CellEditorTextField)((FieldNameCellEditor)e.getSource()).getComponent();
textField.finishedEdit();
}
public void editingStopped(ChangeEvent e) {
FieldNameCellEditor.CellEditorTextField textField = (FieldNameCellEditor.CellEditorTextField)((FieldNameCellEditor)e.getSource()).getComponent();
if (!textField.isEditing()) {
return;
}
int row = textField.getEditedRow();
int col = textField.getEditedColumn();
textField.finishedEdit();
if ((row == -1) || (col == -1)) {
return;
}
FieldName fieldName = RowHeader.this.recordFieldModel.getHasFieldName(row);
if (!textField.isValidFieldName()) {
setValueAt(fieldName.getCalSourceForm(), row, col);
return;
}
String newFieldNameAsString = textField.getText();
if (newFieldNameAsString.equals(fieldName.getCalSourceForm())) {
return;
}
RowHeader.this.headerOwner.renameField(fieldName, FieldName.make(newFieldNameAsString));
RowHeader.this.recordEditor.refreshDisplay();
}
};
}
/**
* Adds the specified focus listener to the table and cell editor
* @param listener
*/
public void addEditorFocusListener(FocusListener listener) {
((FieldNameCellEditor)this.getColumn("fieldName").getCellEditor()).addFocusListener(listener);
}
/**
* Update the header table with the proper field names, in response to a model update.
*/
public void recordFieldModelChanged() {
// Remove all table rows
DefaultTableModel model = ((DefaultTableModel)this.getModel());
for (int i = 0, n = model.getRowCount(); i<n; i++) {
model.removeRow(0);
}
// Get names of all fields and put them in the table
for (int i = 0, n = recordFieldModel.getNHasFields(); i < n; i++) {
List<FieldName> rowElement = Collections.singletonList(recordFieldModel.getHasFieldName(i));
model.addRow(rowElement.toArray());
}
// Set the cell edit listeners of each row header cell; note that the number of cells may have
// increased and reordered, so this editor needs to be set to the new cells this way:
for (int i = 0, n = recordFieldModel.getNHasFields(); i < n; i++) {
getCellEditor(i, 0).removeCellEditorListener(editorListener);
getCellEditor(i, 0).addCellEditorListener(editorListener);
}
}
}
/**
* Class for rendering cells in a table where each row contains a different type
* expression.
*
* This creates a list of ValueEditorTableCellRenderers for rendering each
* type, and simply delegates rendering to the appropriate one based on the row requested.
*
* @author Iulian Radu
*/
static class RowCellRenderer implements TableCellRenderer {
private final List <ValueEditorTableCellRenderer> rowRenderers = new ArrayList<ValueEditorTableCellRenderer>();
/**
* Constructor
*
* @param displayElementNumber whether to display element number in the renderer tooltip
* @param rowTypes types of each element, per row
* @param valueEditorManager
*/
public RowCellRenderer(boolean displayElementNumber,
TypeExpr[] rowTypes,
ValueEditorManager valueEditorManager) {
for (final TypeExpr rowType : rowTypes) {
ValueEditorTableCellRenderer cellRenderer = new ValueEditorTableCellRenderer(displayElementNumber, rowType, valueEditorManager);
rowRenderers.add(cellRenderer);
}
}
/**
* Set editability of the rendered cells
* @param editable whether the cells are editable
*/
public void setEditable(boolean editable) {
for (int i = 0, n = rowRenderers.size(); i < n; i++) {
ValueEditorTableCellRenderer cellRenderer = rowRenderers.get(i);
cellRenderer.setEditable(editable);
}
}
/**
* {@inheritDoc}
*/
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
boolean isSelectedColumn = false;
// Call the respective renderer for this row
// XXX: Note that the renderer uses its ROW to display a tooltip indicating the index of the element;
// since in this layout the elements are displayed in columns, we pass the current column as the
// renderer row.
return rowRenderers.get(row).getTableCellRendererComponent(table, value, isSelectedColumn, hasFocus, column, column);
}
}
/**
* Table header which allows editing of column header cells.
*
* This header is specialized for editing record fields, and it
* uses the same renderers and cell editors as the regular RowHeader.
*/
static class EditableHeader extends JTableHeader implements CellEditorListener, RecordFieldModel.ModelChangeListener {
/**
* Column model for editable columns. This ensures that when a column is added,
* it is converted to an editable column before being added to the model.
*
* @author Iulian Radu
*/
static class EditableHeaderTableColumnModel extends DefaultTableColumnModel {
private static final long serialVersionUID = -1608980349246553152L;
@Override
public void addColumn(TableColumn column) {
EditableHeaderTableColumn newColumn = new EditableHeaderTableColumn();
newColumn.copyValuesFrom(column);
super.addColumn(newColumn);
}
}
/**
* Represents a table column with an editable header.
*/
static class EditableHeaderTableColumn extends TableColumn {
private static final long serialVersionUID = 8939477802657777095L;
/** Whether the column is editable */
protected boolean isHeaderEditable;
/** Editor for this column; null if none */
protected TableCellEditor headerEditor;
/** Constructor */
public EditableHeaderTableColumn() {
isHeaderEditable = true;
}
/**
* Set editability of this column's header
* @param isEditable
*/
public void setHeaderEditable(boolean isEditable) {
isHeaderEditable = isEditable;
}
/**
* @return whether the column header may be edited
*/
public boolean isHeaderEditable() {
return isHeaderEditable;
}
/**
* Copy the values of the base column into ours
* @param base column to copy
*/
void copyValuesFrom(TableColumn base) {
modelIndex = base.getModelIndex();
identifier = base.getIdentifier();
width = base.getWidth();
minWidth = base.getMinWidth();
setPreferredWidth(base.getPreferredWidth());
maxWidth = base.getMaxWidth();
headerRenderer = base.getHeaderRenderer();
headerValue = base.getHeaderValue();
cellRenderer = base.getCellRenderer();
cellEditor = base.getCellEditor();
isResizable = base.getResizable();
}
}
/**
* UI for the editable table header
*/
class EditableHeaderUI extends BasicTableHeaderUI {
/**
* @see javax.swing.plaf.basic.BasicTableHeaderUI#createMouseInputListener()
*/
@Override
protected MouseInputListener createMouseInputListener() {
return new MouseInputHandler((EditableHeader) header);
}
/**
* Handler for mouse input
*/
public class MouseInputHandler extends BasicTableHeaderUI.MouseInputHandler {
private Component dispatchComponent;
protected EditableHeader header;
/** Constructor */
public MouseInputHandler(EditableHeader header) {
this.header = header;
}
/**
* Sets the dispatch component to the object pointed to by the mouse event.
*/
private void setDispatchComponent(MouseEvent e) {
Component editorComponent = header.getEditorComponent();
Point p = e.getPoint();
Point p2 = SwingUtilities.convertPoint(header, p, editorComponent);
dispatchComponent = SwingUtilities.getDeepestComponentAt(
editorComponent, p2.x, p2.y);
}
/** Post the mouse event to the dispatch component */
private boolean repostEvent(MouseEvent e) {
if (dispatchComponent == null) {
return false;
}
MouseEvent e2 = SwingUtilities.convertMouseEvent(header, e,
dispatchComponent);
dispatchComponent.dispatchEvent(e2);
return true;
}
/**
* Edit the header column on mouse click
* @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
*/
@Override
public void mousePressed(MouseEvent e) {
if (!SwingUtilities.isLeftMouseButton(e)) {
return;
}
super.mousePressed(e);
if (header.getResizingColumn() == null) {
Point p = e.getPoint();
TableColumnModel columnModel = header.getColumnModel();
int index = columnModel.getColumnIndexAtX(p.x);
if (index != -1) {
header.processEvent(new FocusEvent(e.getComponent(), FocusEvent.FOCUS_GAINED));
if (header.editCellAt(index, e)) {
setDispatchComponent(e);
repostEvent(e);
}
header.getTable().scrollRectToVisible(header.getHeaderRect(index));
}
}
}
/**
* @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
*/
@Override
public void mouseReleased(MouseEvent e) {
super.mouseReleased(e);
if (!SwingUtilities.isLeftMouseButton(e)) {
return;
}
repostEvent(e);
dispatchComponent = null;
}
}
}
private static final long serialVersionUID = 6343949193489544440L;
/** Holds index of the column currently being edited */
private int editingColumn;
/** Index of the column currently selected on the header (ie: the last column edited) */
private int selectedColumn;
/** The table cell renderer for displaying header cells */
private final RowHeader.CellRenderer cellRenderer;
/** The table cell editor used to edit header cells */
private final RowHeader.FieldNameCellEditor cellEditor;
/** Editor component displayed for doing the editing (ie: the editing text field) */
protected Component editorComp;
/** Header owner, used for notification of field renames */
private final RowHeader.RowHeaderOwner headerOwner;
/** Model holding information about record fields. */
private final RecordFieldModel recordFieldModel;
/**
* Constructor
*
* @param columnModel column model of the table we are attaching to
* @param recordEditor value editor owning this header
* @param headerOwner owner of the header, performing renames
* @param recordFieldModel field model of the record edited
*/
public EditableHeader(TableColumnModel columnModel, ValueEditor recordEditor, RowHeader.RowHeaderOwner headerOwner, RecordFieldModel recordFieldModel) {
super(columnModel);
setReorderingAllowed(false);
this.headerOwner = headerOwner;
this.recordFieldModel = recordFieldModel;
recordFieldModel.addModelChangeListener(this);
this.cellRenderer = new RowHeader.CellRenderer(recordEditor, false, headerOwner, recordFieldModel);
cellEditor = new RowHeader.FieldNameCellEditor(recordFieldModel, false);
cellEditor.setClickCountToStart(1);
cellEditor.addCellEditorListener(this);
this.setFocusable(true);
}
/**
* @see javax.swing.JComponent#updateUI()
*/
@Override
public void updateUI() {
setUI(new EditableHeaderUI());
resizeAndRepaint();
invalidate();
}
/**
* Initialize the cell editor and renderer for the table columns.
* Use this when the field model or editor layout changes.
*/
public void recordFieldModelChanged() {
updateColumnHeaders();
}
/**
* Updates the headers of the model columns to have proper renderer and editability.
*/
private void updateColumnHeaders() {
for (int i = 0, n = getColumnModel().getColumnCount(); i < n; i++) {
EditableHeaderTableColumn column = (EditableHeaderTableColumn)getColumnModel().getColumn(i);
column.setHeaderEditable(!recordFieldModel.isContextField(FieldName.make((String)column.getHeaderValue())));
column.setHeaderRenderer(cellRenderer);
}
}
/**
* Sets the column model and updates the headers of the columns.
* @see javax.swing.table.JTableHeader#setColumnModel(javax.swing.table.TableColumnModel)
*/
@Override
public void setColumnModel(TableColumnModel columnModel) {
super.setColumnModel(columnModel);
updateColumnHeaders();
}
/**
* Sets the table and updates the headers of the columns.
* @see javax.swing.table.JTableHeader#setTable(javax.swing.JTable)
*/
@Override
public void setTable(JTable table) {
super.setTable(table);
updateColumnHeaders();
}
/**
* Invoked when an event invokes editing of a certain cell
* @param index index of header cell
* @param e invoker event
* @return true if editing should start; false if not
*/
public boolean editCellAt(int index, EventObject e) {
if (cellEditor != null && (isEditing() && !cellEditor.stopCellEditing())) {
return false;
}
if (!isCellEditable(index)) {
return false;
}
TableCellEditor editor = cellEditor;
if (editor != null && editor.isCellEditable(e)) {
editorComp = prepareEditor(editor, index);
((RowHeader.FieldNameCellEditor.CellEditorTextField)((RowHeader.FieldNameCellEditor)editor).getComponent()).initializeEdit(0, index);
editorComp.setBounds(getHeaderRect(index));
add(editorComp);
editorComp.validate();
setEditingColumn(index);
setSelectedColumn(index);
return true;
}
return false;
}
/**
* Indicates if the cell is editable
* @param index index of header cell
* @return true if editable
*/
public boolean isCellEditable(int index) {
if (getReorderingAllowed()) {
return false;
}
int columnIndex = columnModel.getColumn(index).getModelIndex();
EditableHeaderTableColumn col = (EditableHeaderTableColumn) columnModel
.getColumn(columnIndex);
return col.isHeaderEditable();
}
/**
* Prepare the editor for editing
* @param editor cell editor
* @param index index of the field we are about to edit
* @return component which will do the actual editing
*/
private Component prepareEditor(TableCellEditor editor, int index) {
Object value = columnModel.getColumn(index).getHeaderValue();
boolean isSelected = true;
JTable table = getTable();
Component comp = editor.getTableCellEditorComponent(table, value,
isSelected, 0, index);
if (comp instanceof JComponent) {
((JComponent)comp).setNextFocusableComponent(this);
}
return comp;
}
/**
* @return the component doing the editing
*/
private Component getEditorComponent() {
return editorComp;
}
/**
* Set index of the current column being edited
* @param aColumn
*/
private void setEditingColumn(int aColumn) {
editingColumn = aColumn;
}
/**
* Set index of the selected column
* @param aColumn
*/
private void setSelectedColumn(int aColumn) {
selectedColumn = aColumn;
}
/**
* @return index of the column being edited
*/
private int getEditingColumn() {
return editingColumn;
}
/**
* @return index of the last edited column
*/
public int getSelectedColumn() {
return selectedColumn;
}
/**
* @return the record field model observed
*/
public RecordFieldModel getRecordFieldModel() {
return recordFieldModel;
}
/**
* Removes the editor component and the area it covers
*/
private void removeEditorRect() {
if (editorComp != null) {
remove(editorComp);
int index = getEditingColumn();
Rectangle cellRect = getHeaderRect(index);
setEditingColumn(-1);
editorComp = null;
repaint(cellRect);
}
}
/**
* @return whether the editor is currently editing a column
*/
private boolean isEditing() {
return (getEditingColumn() != -1);
}
/**
* {@inheritDoc}
*/
public void editingStopped(ChangeEvent e) {
TableCellEditor editor = cellEditor;
if (editor != null && getEditorComponent() != null) {
int index = getEditingColumn();
removeEditorRect();
// Now update the field name
if (index == -1) {
return;
}
FieldName oldFieldName = recordFieldModel.getHasFieldName(index);
RowHeader.FieldNameCellEditor.CellEditorTextField textField = (RowHeader.FieldNameCellEditor.CellEditorTextField)((RowHeader.FieldNameCellEditor)e.getSource()).getComponent();
String newFieldNameAsString = textField.getText();
if (!textField.isValidFieldName() || newFieldNameAsString.equals(oldFieldName.getCalSourceForm())) {
columnModel.getColumn(index).setHeaderValue(oldFieldName.getCalSourceForm());
return;
}
headerOwner.renameField(oldFieldName, FieldName.make(newFieldNameAsString));
}
}
/**
* {@inheritDoc}
*/
public void editingCanceled(ChangeEvent e) {
removeEditorRect();
}
}
private static final long serialVersionUID = -2376095137973719167L;
/** The minimum size of this editor */
private static final Dimension MIN_SIZE = new Dimension(200, -1);
/** The maximum size of this editor. */
private static final Dimension MAX_SIZE = new Dimension(600, 400);
/**
* The default prefix for a new record field name.
* If clashing with an existing field name, this will be appended with an incremental numeral
* until the clash is resolved. In effect, we are always creating ordinal field names.
*/
static final String DEFAULT_NEW_FIELD_PREFIX = "#";
/**
* Whether the layout of this editor is allowed to be switchable between horizontal and vertical.
* The default is verticalLayout, where fields correspond to rows.
*/
private static final boolean LAYOUT_SWITCH_ALLOWED = false;
/** Action for adding a record field */
private Action addAction;
/** Action for removing a record field */
private Action deleteAction;
/** Table indicating record fields as row headers */
private final RowHeader rowHeader;
/** Scroll pane for the row header */
private final JScrollPane rowHeaderScrollPane;
/** Indicates if this editor has been initialized once */
private boolean initializedOnce = false;
/** Main panel holding row header scroll pane on the left side, and table on the right */
private final JSplitPane dividerPanel;
/** Icon indicating the record type */
private JLabel typeIcon;
/** Whether the record is a record polymorphic type, (ie: whether record fields may be added/removed) */
private boolean isBasePolymorphicRecord = true;
/** Panel containing the editor buttons */
private final JPanel buttonPanel;
/** Panel replacing the divider panel, when the record has no fields */
private final JPanel noFieldsPanel;
/**
* Whether the table orientation is vertical.
* If true, fields are displayed in individual columns; otherwise, they are
* displayed in individual rows.
*/
private boolean verticalLayout = false;
/** Action for switching between horizontal and vertical layout */
private Action switchAction;
/** Model which tracks the fields in the record type, observed by components representing such fields */
private final RecordFieldModel recordFieldModel;
/**
* RecordValueEditor constructor
* @param valueEditorHierarchyManager
* @param dragPointHandler
*/
protected RecordValueEditor(ValueEditorHierarchyManager valueEditorHierarchyManager,
RecordValueDragPointHandler dragPointHandler) {
super(valueEditorHierarchyManager);
this.recordFieldModel = new RecordFieldModel();
// Initialize button panel
buttonPanel = new JPanel(new GridBagLayout());
JButton addButton = new JButton(getAddAction());
JButton deleteButton = new JButton(getDeleteAction());
{
{
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridx = 0;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.weightx = 0.0;
constraints.weighty = 0.0;
constraints.fill = GridBagConstraints.HORIZONTAL;
buttonPanel.add(getTypeIcon(), constraints);
}
{
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridx = 1;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.weightx = 1.0;
constraints.weighty = 0.0;
constraints.fill = GridBagConstraints.HORIZONTAL;
buttonPanel.add(new JLabel(), constraints);
}
{
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridx = 2;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.weightx = 0.0;
constraints.weighty = 0.0;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.insets = new Insets(5,5,5,5);
buttonPanel.add(addButton, constraints);
}
{
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridx = 3;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.weightx = 0.0;
constraints.weighty = 0.0;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.insets = new Insets(5,5,5,5);
buttonPanel.add(deleteButton, constraints);
}
// Put the Switch button only if layout switching is enabled
if (LAYOUT_SWITCH_ALLOWED) {
JButton switchButton = new JButton(getSwitchAction());
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridx = 4;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.weightx = 0.0;
constraints.weighty = 0.0;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.insets = new Insets(5,5,5,5);
buttonPanel.add(switchButton, constraints);
}
}
// Initialize main panel and headers
dividerPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
dividerPanel.setDividerSize(2);
// Create header listener for renames
RowHeader.RowHeaderOwner rowHeaderOwner = new RowHeader.RowHeaderOwner() {
public TypeExpr getFieldType(int row) {
return ((AbstractRecordValueNode)getValueNode()).getFieldTypeExpr(((AbstractRecordValueNode)getValueNode()).getFieldName(row));
}
public void renameField(FieldName oldName, FieldName newName) {
replaceValueNode(((AbstractRecordValueNode)getValueNode()).renameField(oldName, newName, valueEditorManager.getValueNodeBuilderHelper(), valueEditorManager.getValueNodeTransformer()), true);
int fieldIndex = getValueNode().getTypeExpr().rootRecordType().getHasFieldNames().indexOf(newName);
selectCell(fieldIndex, 0);
}
};
// Create row header
rowHeader = new RowHeader(this, table, rowHeaderOwner, recordFieldModel);
rowHeader.addEditorFocusListener(new FocusListener() {
public void focusGained(FocusEvent e) {
RecordValueEditor.this.clearSelection(true);
enableButtons();
}
public void focusLost(FocusEvent e) {
}
});
// Create panels and padding for the row header
// The row header scroll panel (which is the left component of the dividePanel) contains the
// following components:
// - (rowHeaderPaddedInnerPanel) a panel containing
// - (rowHeader) the table row header (ie: a table whose cells indicate row indices)
// - (l1) a label padding underneath the table (this becomes visible when the panel is stretched vertically
// past the table height)
// - (l2) a label padding underneath the panel (this becomes visible only when there is a horizontal scrollbar
// on the right side of the divider)
JPanel rowHeaderPaddedPanel = new JPanel(new BorderLayout());
JPanel rowHeaderPaddedInnerPanel = new JPanel(new BorderLayout());
rowHeaderPaddedInnerPanel.add(rowHeader, BorderLayout.NORTH);
JLabel l1 = new JLabel(" ");
l1.setPreferredSize(new Dimension(0,0));
rowHeaderPaddedInnerPanel.add(l1, BorderLayout.CENTER);
rowHeaderPaddedPanel.add(rowHeaderPaddedInnerPanel, BorderLayout.CENTER);
rowHeaderPaddedPanel.setBorder(BorderFactory.createEmptyBorder());
rowHeaderScrollPane = new JScrollPane(rowHeaderPaddedPanel);
JLabel l2 = new JLabel(" ");
l2.setPreferredSize(new Dimension(0,0));
rowHeaderPaddedPanel.add(l2, BorderLayout.SOUTH);
rowHeaderScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
rowHeaderScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
rowHeaderScrollPane.setBorder(BorderFactory.createEmptyBorder());
rowHeaderScrollPane.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
table.setTableHeader(null);
tableScrollPane.setBorder(BorderFactory.createEmptyBorder());
// Place the row header on the left and table scroll pane on the right
dividerPanel.setRightComponent(tableScrollPane);
dividerPanel.setLeftComponent(rowHeaderScrollPane);
dividerPanel.setBorder(BorderFactory.createLoweredBevelBorder());
// Add listeners to scroll both table and row header when one scrolls
rowHeaderScrollPane.getViewport().addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
tableScrollPane.getViewport().setViewPosition(new Point(tableScrollPane.getViewport().getViewPosition().x, ((JViewport)e.getSource()).getViewPosition().y));
}
});
tableScrollPane.getViewport().addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
rowHeaderScrollPane.getViewport().setViewPosition(new Point(rowHeaderScrollPane.getViewport().getViewPosition().x, ((JViewport)e.getSource()).getViewPosition().y));
}
});
// Attach a listener for drag events
if (dragPointHandler != null) {
JTableHeader tableHeader = getTableHeader();
DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(
tableHeader,
DnDConstants.ACTION_COPY_OR_MOVE,
null);
}
setLayout(new BorderLayout());
add(buttonPanel, BorderLayout.SOUTH);
noFieldsPanel = new JPanel(new BorderLayout());
JLabel noFieldsLabel = new JLabel(ValueEditorMessages.getString("VE_NoFieldsLabel"));
noFieldsLabel.setPreferredSize(new Dimension(noFieldsLabel.getPreferredSize().width, table.getRowHeight() + 5));
noFieldsLabel.setBorder(BorderFactory.createLoweredBevelBorder());
noFieldsPanel.add(noFieldsLabel, BorderLayout.CENTER);
add(noFieldsPanel, BorderLayout.CENTER);
add(dividerPanel, BorderLayout.CENTER);
}
/**
* Note: If the editor type is a records which is non-record-polymorphic
* then this method ensures that the type becomes record-polymorphic if the context allows.
* Ex: If the record 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() {
if (!initializedOnce) {
initSize(MIN_SIZE, MAX_SIZE);
initializedOnce = true;
}
// If we are not constrained by the context, make sure our type is not record polymorphic,
// This is an allowed operation because we are actually specializing the type.
// TODO: Enforce non-record-polymorphic regardless of context.
// Because the CAL value produced by the value node is always non-record-polymorphic, the
// type of this value entry panel should be consistent.
//
// 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 contextRecordType = getContext().getLeastConstrainedTypeExpr();
if (contextRecordType.rootTypeVar() != null) {
if (getValueNode().getTypeExpr().rootRecordType().isRecordPolymorphic()) {
// Not constrained by context, so create new type expression according to the fields we have
Map<FieldName, TypeExpr> fieldNamesToTypeMap = new HashMap<FieldName, TypeExpr>();
List<FieldName> hasFields = getValueNode().getTypeExpr().rootRecordType().getHasFieldNames();
for (final FieldName hasFieldName : hasFields) {
TypeExpr hasFieldType = getValueNode().getTypeExpr().rootRecordType().getHasFieldType(hasFieldName);
fieldNamesToTypeMap.put(hasFieldName, hasFieldType);
}
RecordType newRecordType = TypeExpr.makeNonPolymorphicRecordType(fieldNamesToTypeMap);
replaceValueNode(getValueNode().transmuteValueNode(valueEditorManager.getValueNodeBuilderHelper(), valueEditorManager.getValueNodeTransformer(), newRecordType), true);
notifyValueChanged(getValueNode());
}
}
isBasePolymorphicRecord = contextRecordType.rootTypeVar() != null ? true : contextRecordType.rootRecordType().isRecordPolymorphic();
setButtonPanelVisible(isBasePolymorphicRecord);
// Update the field model
recordFieldModel.initialize((RecordType)getValueNode().getTypeExpr(), getContext().getLeastConstrainedTypeExpr());
// Update the UI
initializeUI();
refreshMinMaxDimensions();
}
/**
* Refresh the minimum and maximum resize dimensions of this editor, according to the
* number of fields displayed.
*/
private void refreshMinMaxDimensions() {
// Initialize max/min size
tableScrollPane.setPreferredSize(table.getPreferredSize());
setMaxResizeDimension(new Dimension(2048, Math.min(MAX_SIZE.height, getPreferredSize().height)));
setMinResizeDimension(new Dimension(Math.min(getMaxResizeDimension().width, getMinimumSize().width),
Math.min(getMaxResizeDimension().height, Math.max(MIN_SIZE.height, getPreferredSize().height))));
Dimension minResizeDimension = getMinResizeDimension();
Dimension maxResizeDimension = getMaxResizeDimension();
Dimension currentSize = getSize();
currentSize.width = ValueEditorManager.clamp(minResizeDimension.width, currentSize.width, maxResizeDimension.width);
currentSize.height = ValueEditorManager.clamp(minResizeDimension.height, currentSize.height, maxResizeDimension.height);
setSize(currentSize);
validate();
}
/**
* Initialize UI components. This adds the appropriate UI components to the
* value editor, depending on the number of fields and orientation.
*/
private void initializeUI() {
if (((AbstractRecordValueNode)getValueNode()).getNFieldNames() > 0) {
// We have some fields in the record
if (!verticalLayout) {
// Displaying fields on rows, via rowHeader
dividerPanel.setRightComponent(tableScrollPane);
// Set minimum size for the header
int maxWidth = 0;
List<FieldName> fieldNames = getValueNode().getTypeExpr().rootRecordType().getHasFieldNames();
for (final FieldName fieldName : fieldNames) {
int width = SwingUtilities.computeStringWidth(rowHeader.getFontMetrics(rowHeader.getFont()), fieldName.getCalSourceForm());
maxWidth = Math.max(maxWidth, width);
}
maxWidth += 30; // longest (to display) field + 30
// Initialize divider
int horizontalDividerLocation = Math.max(rowHeader.getMinimumSize().width, maxWidth);
dividerPanel.setDividerLocation(horizontalDividerLocation);
// Make sure when we have some fields, we don't display the "No Fields" panel
noFieldsPanel.setVisible(false);
dividerPanel.setVisible(true);
remove(noFieldsPanel);
add(dividerPanel, BorderLayout.CENTER);
} else {
// Displaying fields in columns
remove(dividerPanel);
add(tableScrollPane, BorderLayout.CENTER);
}
} else {
// Make sure when we have no fields, we display the "No Fields" panel
noFieldsPanel.setVisible(true);
dividerPanel.setVisible(false);
remove(dividerPanel);
add(noFieldsPanel, BorderLayout.CENTER);
}
enableButtons();
validate();
}
/**
* Set visibility of the button panel as specified
*/
private void setButtonPanelVisible(boolean visible) {
for (int i = 0, n = buttonPanel.getComponentCount(); i < n; i++) {
buttonPanel.getComponent(i).setVisible(visible);
}
}
/**
* @see org.openquark.gems.client.valueentry.ValueEditor#refreshDisplay()
*/
@Override
public void refreshDisplay() {
// Reselect the cell that was previously being edited.
selectCell(getSelectedRow(), getSelectedColumn());
updateTypeIcon();
}
/** Disable the remove button if a field is not removable/selected */
@Override
public void handleCellActivated() {
super.handleCellActivated();
enableButtons();
}
/** Enables or disables the remove button if the selected field can be edited */
public void enableButtons() {
int selectedRow = getSelectedRow();
if (selectedRow == -1) {
selectedRow = rowHeader.getSelectedRow();
}
if (selectedRow != -1 && table.getRowCount() > selectedRow && rowHeader.getColumnCount() > 0 && rowHeader.getRowCount() > 0 && rowHeader.isCellEditable(selectedRow, 0)) {
getDeleteAction().setEnabled(true);
} else {
getDeleteAction().setEnabled(false);
}
}
/**
* @see org.openquark.gems.client.valueentry.TableValueEditor#createTableModel(org.openquark.cal.valuenode.ValueNode)
*/
@Override
protected ValueEditorTableModel createTableModel(ValueNode valueNode) {
return new RecordTableModel((AbstractRecordValueNode)valueNode, valueEditorHierarchyManager, verticalLayout);
}
/**
* 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 to hold the result of this function ValueNode->TypeExpr
final Map<ValueNode, TypeExpr> resultMap = new HashMap<ValueNode, TypeExpr>();
final AbstractRecordValueNode currentValueNode = (AbstractRecordValueNode)getValueNode();
final List<FieldName> hasFieldNames = ((RecordType)currentValueNode.getTypeExpr()).getHasFieldNames();
final int numFields = currentValueNode.getTypeExpr().rootRecordType().getNHasFields();
// get the context least constrained type
final TypeExpr contextLeastConstrainedType = getContext().getLeastConstrainedTypeExpr();
//build one that includes all of the new fields as well.
final Map<FieldName, TypeExpr> contextFieldsMap; //map of all the fields in the context RecordFieldName -> TypeExpr
if (contextLeastConstrainedType instanceof RecordType) {
contextFieldsMap = ((RecordType) contextLeastConstrainedType).getHasFieldsMap();
} else {
contextFieldsMap = new HashMap<FieldName, TypeExpr>();
}
final Map<FieldName, TypeExpr> allFieldsMap = new HashMap<FieldName, TypeExpr>(); //map containing all the context fields and fields added with the value editor, RecordFieldName -> TypeExpr
for (int i = 0; i < numFields; i++) {
final FieldName fieldName = hasFieldNames.get(i);
if (contextFieldsMap.containsKey(fieldName)) {
allFieldsMap.put(fieldName, contextFieldsMap.get(fieldName));
} else {
allFieldsMap.put(fieldName, TypeExpr.makeParametricType());
}
}
final RecordType leastConstrainedRecordType = TypeExpr.makePolymorphicRecordType(allFieldsMap);
// add the record node itself to the result map
resultMap.put(currentValueNode, leastConstrainedRecordType);
// add all the record field nodes to the result map
for (int j = 0; j < numFields; j++) {
ValueNode currentFieldNode = currentValueNode.getValueAt(j);
FieldName fieldName = hasFieldNames.get(j);
TypeExpr leastConstrainedRecordElementType = leastConstrainedRecordType.getHasFieldType(fieldName);
resultMap.put(currentFieldNode, leastConstrainedRecordElementType);
}
return resultMap;
}
/**
* {@inheritDoc}
*/
@Override
public void commitChildChanges(ValueNode oldChild, ValueNode newChild) {
// Get the copy of the current value node, type switched if necessary.
ValueNode oldValueNode = getValueNode();
AbstractRecordValueNode newValueNode;
if (!oldChild.getTypeExpr().sameType(newChild.getTypeExpr())) {
// A child is switching type. So calculate new types/values of all our field nodes
Map<ValueNode, TypeExpr> valueNodeToUnconstrainedTypeMap = getValueNodeToUnconstrainedTypeMap();
Map<ValueNode, ValueNode> commitValueMap = valueEditorManager.getValueNodeCommitHelper().getCommitValues(oldChild, newChild, valueNodeToUnconstrainedTypeMap);
newValueNode = (AbstractRecordValueNode)commitValueMap.get(oldValueNode);
} else {
newValueNode = (AbstractRecordValueNode)oldValueNode.copyValueNode();
}
// Update the values of the children to match the updated value
List<ValueNode> currentChildrenList = UnsafeCast.<List<ValueNode>>unsafeCast(oldValueNode.getValue()); // unsafe.
for (int i = 0, listSize = newValueNode.getTypeExpr().rootRecordType().getNHasFields(); i < listSize; i++) {
if (currentChildrenList.get(i) == oldChild) {
TypeExpr childType = newValueNode.getValueAt(i).getTypeExpr();
newValueNode.setValueNodeAt(i, newChild.copyValueNode(childType));
break;
}
}
// Set the value node
replaceValueNode(newValueNode, true);
}
/**
* @return action for switching layout
*/
private Action getSwitchAction() {
if (switchAction == null) {
switchAction = new AbstractAction("SWITCH") {
private static final long serialVersionUID = 1676926437073351056L;
public void actionPerformed(ActionEvent evt) {
int selectedRow = getSelectedRow();
int selectedColumn = getSelectedColumn();
verticalLayout = !verticalLayout;
performUpdatesForSwitch();
replaceValueNode(getValueNode(), true);
initializeUI();
userHasResized();
selectCell(selectedColumn, selectedRow);
}
};
}
return switchAction;
}
/**
* Updates UI components after a layout switch is performed
*/
private void performUpdatesForSwitch() {
// Update the model with the proper layout
setTableModel(createTableModel(getValueNode()));
// Initialize table header
initializeTableCellRenderers();
validate();
}
/**
* {@inheritDoc}
*/
@Override
protected void userHasResized() {
if (!verticalLayout) {
return;
}
Dimension tableScrollPaneSize = tableScrollPane.getSize();
int newResizeMode;
if (tableScrollPaneSize.width >= (75 * table.getColumnCount())) {
newResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS;
} else {
newResizeMode = JTable.AUTO_RESIZE_OFF;
}
if (newResizeMode != table.getAutoResizeMode()) {
table.setAutoResizeMode(newResizeMode);
dividerPanel.revalidate();
}
}
/**
* @return action for adding a record field
*/
private Action getAddAction() {
if (addAction == null) {
addAction = new AbstractAction(ValueEditorMessages.getString("VE_AddButtonLabel")) {
private static final long serialVersionUID = 1229365986103881376L;
public void actionPerformed(ActionEvent evt) {
if (!isBasePolymorphicRecord) {
System.out.println("Cannot add field - not polymorphic record");
return;
}
// Create a field name which does not conflict with existing or lacks fields
List<FieldName> hasFields = getValueNode().getTypeExpr().rootRecordType().getHasFieldNames();
Set<FieldName> lacksFields = getValueNode().getTypeExpr().rootRecordType().getLacksFieldsSet();
FieldName newFieldName = RecordValueEditor.getNewFieldName(hasFields, lacksFields);
// Create new record type having the existing fields plus a new one
Map<FieldName, TypeExpr> fieldNamesToTypeMap = new HashMap<FieldName, TypeExpr>();
for (final FieldName hasFieldName : hasFields) {
TypeExpr hasFieldType = getValueNode().getTypeExpr().rootRecordType().getHasFieldType(hasFieldName);
fieldNamesToTypeMap.put(hasFieldName, hasFieldType);
}
fieldNamesToTypeMap.put(newFieldName, TypeExpr.makeParametricType());
RecordType newRecordType = TypeExpr.makeNonPolymorphicRecordType(fieldNamesToTypeMap);
replaceValueNode(getValueNode().transmuteValueNode(valueEditorManager.getValueNodeBuilderHelper(), valueEditorManager.getValueNodeTransformer(), newRecordType), true);
// Select the new field
int fieldIndex = getValueNode().getTypeExpr().rootRecordType().getHasFieldNames().indexOf(newFieldName);
selectCell(fieldIndex, 0);
}
};
addAction.putValue(Action.SHORT_DESCRIPTION, ValueEditorMessages.getString("VE_AddRecordField"));
}
return addAction;
}
/**
* @return action for removing a record field
*/
private Action getDeleteAction() {
if (deleteAction == null) {
deleteAction = new AbstractAction(ValueEditorMessages.getString("VE_RemoveButtonLabel")) {
private static final long serialVersionUID = -5743671496229434840L;
public void actionPerformed(ActionEvent evt) {
if (!isBasePolymorphicRecord) {
throw new IllegalStateException();
}
// Determine the index of the deleted field
int row = getSelectedRow();
int col = getSelectedColumn();
if (row == -1) {
row = rowHeader.getSelectedRow();
col = rowHeader.getSelectedColumn();
}
if ((row == -1) || (col == -1)) {
return;
}
if (!rowHeader.getModel().isCellEditable(row, 0)) {
throw new IllegalStateException();
}
FieldName fieldName = ((AbstractRecordValueNode)getValueNode()).getFieldName(row);
// Create new record type lacking the field
Map<FieldName, TypeExpr> fieldNamesToTypeMap = new HashMap<FieldName, TypeExpr>();
List<FieldName> hasFields = getValueNode().getTypeExpr().rootRecordType().getHasFieldNames();
if (!hasFields.remove(fieldName)) {
throw new IllegalStateException();
}
for (final FieldName hasFieldName : hasFields) {
TypeExpr hasFieldType = getValueNode().getTypeExpr().rootRecordType().getHasFieldType(hasFieldName);
fieldNamesToTypeMap.put(hasFieldName, hasFieldType);
}
RecordType newRecordType = TypeExpr.makeNonPolymorphicRecordType(fieldNamesToTypeMap);
replaceValueNode(getValueNode().transmuteValueNode(valueEditorManager.getValueNodeBuilderHelper(), valueEditorManager.getValueNodeTransformer(), newRecordType), true);
clearSelection(true);
enableButtons();
clearSelection(false);
// Handle selection
int rowCount = getRowCount();
if (rowCount != 0) {
int newSelectedRow = 0;
// Update the highlighted/selected row and edited cell.
if (rowCount == row) {
// Move highlight/selection up one row, since we just deleted the last row.
newSelectedRow = row - 1;
} else {
// Keep the selection in its old row.
newSelectedRow = row;
}
selectCell(newSelectedRow, 0);
}
}
};
deleteAction.putValue(Action.SHORT_DESCRIPTION, ValueEditorMessages.getString("VE_RemoveRecordField"));
}
return deleteAction;
}
/**
* @see org.openquark.gems.client.valueentry.ValueEditor#setEditable(boolean)
*/
@Override
public void setEditable(boolean editable) {
getAddAction().setEnabled(editable);
getDeleteAction().setEnabled(editable);
setButtonPanelVisible(isBasePolymorphicRecord);
}
/**
* Updates the icon and tooltip of the editor type icon.
*/
private void updateTypeIcon() {
TypeExpr typeExpr = getValueNode().getTypeExpr();
String iconName = valueEditorManager.getTypeIconName(typeExpr);
getTypeIcon().setIcon(new ImageIcon(ListTupleValueEditor.class.getResource(iconName)));
getTypeIcon().setToolTipText("<html><body>" + ToolTipHelpers.wrapTextToHTMLLines(typeExpr.toString(), getTypeIcon()) + "</body></html>");
}
/**
* Return the ElementIcon property value.
* Note: Extra set-up code has been added.
* @return JLabel
*/
private JLabel getTypeIcon() {
if (typeIcon == null) {
typeIcon = new JLabel();
typeIcon.setIcon(new ImageIcon(getClass().getResource("/Resources/notype.gif")));
typeIcon.setBorder(new EmptyBorder(0, 3, 0, 0));
}
return typeIcon;
}
/**
* Initializes table cell and column renderers according to the layout orientation.
* @see org.openquark.gems.client.valueentry.TableValueEditor#initializeTableCellRenderers()
*/
@Override
protected void initializeTableCellRenderers() {
if (verticalLayout) {
// We use the superclass if the layout is vertical
super.initializeTableCellRenderers();
return;
}
TypeExpr typeExpr = getValueNode().getTypeExpr();
TableColumnModel tableColumnModel = table.getColumnModel();
TypeExpr[] elementTypeExprArray = ((RecordTableModel)tableModel).getRowElementTypeExprArray();
boolean displayElementNumber = typeExpr.isListType();
// Set row renderer to have different types for each row
RowCellRenderer cellRenderer = new RowCellRenderer(displayElementNumber, elementTypeExprArray, getValueEditorHierarchyManager().getValueEditorManager());
cellRenderer.setEditable(isEditable());
for (int i = 0, colCount = table.getColumnCount(); i < colCount; i++) {
TableColumn tableColumn = tableColumnModel.getColumn(i);
tableColumn.setCellRenderer(cellRenderer);
}
// Set columns to have no header
for (int i = 0, columnCount = tableColumnModel.getColumnCount(); i < columnCount; i++) {
TableCellRenderer headerRenderer =
createTableHeaderRenderer(null);
TableColumn tableColumn = tableColumnModel.getColumn(i);
tableColumn.setHeaderRenderer(headerRenderer);
}
}
/**
* Replace the value node and perform necessary UI updates.
* @see org.openquark.gems.client.valueentry.ValueEditor#replaceValueNode(org.openquark.cal.valuenode.ValueNode, boolean)
*/
@Override
public void replaceValueNode(ValueNode newValueNode, boolean preserveInfo) {
super.replaceValueNode(newValueNode, preserveInfo);
// Update the field model
recordFieldModel.initialize((RecordType)getValueNode().getTypeExpr(), getContext().getLeastConstrainedTypeExpr());
// Update the UI
initializeUI();
refreshMinMaxDimensions();
enableButtons();
updateTypeIcon();
}
/**
* A helper function to create a new RecordFieldName that does not duplicate an existing
* hasField or lacksField. The algorithm is to try successive ordinal field names '#1', '#2', '#3', '#4', ...
* until a new one is found.
* @param hasFields
* @param lacksFields
* @return RecordFieldName
*/
static FieldName getNewFieldName(Collection<FieldName> hasFields, Collection<FieldName> lacksFields) {
for (int i = 1; true; ++i) {
FieldName fieldName = FieldName.make(RecordValueEditor.DEFAULT_NEW_FIELD_PREFIX + i);
if (fieldName != null &&
!hasFields.contains(fieldName) &&
!lacksFields.contains(fieldName)) {
return fieldName;
}
}
}
}