/*
* 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.
*/
/*
* RecordFieldSelectionGemFieldNameEditor.java
* Creation date: Dec 13, 2006.
* By: Neil Corkum
*/
package org.openquark.gems.client;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.ComboBoxEditor;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.openquark.cal.compiler.FieldName;
import org.openquark.cal.compiler.LanguageInfo;
import org.openquark.cal.compiler.RecordType;
import org.openquark.cal.compiler.TypeExpr;
import org.openquark.gems.client.Gem.PartInput;
import org.openquark.gems.client.utilities.ExtendedUndoManager;
/**
* This class is responsible for determining the correct field name editor to
* display for a given Record Field Selection Gem. The editor provided will depend on the
* types connected to the Record Field Selection Gem, if any.
* @author Neil Corkum
*/
public class RecordFieldSelectionGemFieldNameEditor {
/**
* The RecordFieldSelection Gem this editor will be acting on.
*/
private final RecordFieldSelectionGem recordFieldSelectionGem;
/** this is used to record the fixed state before any modifications are made
* so that the gem can be restored to it's original state if the user aborts the
* edit.
*/
private final boolean oldFixedState;
/** this is used to record the select field name before any modifications are made
* so that the gem can be restored to it's original state if the user aborts the
* edit.
*/
private final String oldFieldName;
/**
* TableTop reference.
*/
private final TableTop tableTop;
/**
* This interface is used so the combo box editor can listen for special text events -
* when the text is updated so that the combo box can be resized, and when
* the editor is finished so that the combo box can close.
*/
private interface TextListener {
/**
* This is called when the text is edited and the editor has resized
* @param dimension
*/
void resize(Dimension dimension);
/**
* This is called when the enter or escape is pressed and the editor is closing.
*/
void closing();
}
/**
* An editable text field that accepts valid CAL identifiers for field names
* @author Neil Corkum
*/
private class RecordFieldSelectionTextEditor extends EditableIdentifierNameField {
//a set containing field names that are disallowed - i.e. in the lacks constraints
final private Set<FieldName> disallowedFields;
//listener to receive test editor messages - resize and select
public TextListener textListener = null;
private static final long serialVersionUID = -3629150370610668038L;
/** Keeps track of whether this component has been removed from the tableTop */ // is there a better way??
private boolean removed = false;
/** The undo manager for this text field. */
private final ExtendedUndoManager undoManager;
/**
* Constructor for a new EditableGemNameField.
*/
public RecordFieldSelectionTextEditor(Set<FieldName> disallowedFields) {
super();
this.disallowedFields = disallowedFields;
String initialText = oldFieldName;
// set the initial text
setInitialText(initialText);
setText(initialText);
// set the font of the text
setFont(GemCutterPaintHelper.getTitleFont());
// update the size of the text area to reflect the size of the text
updateSize();
// starts out with all text selected
selectAll();
// set up the undo manager
undoManager = new ExtendedUndoManager();
getDocument().addUndoableEditListener(undoManager);
// moving focus away commits the text entered and closes this component
addFocusListener(new FocusAdapter(){
@Override
public void focusLost(FocusEvent e) {
commitText();
}
});
// intercept some key events
addKeyListener(new KeyAdapter(){
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
// pressing "ESC" cancels text entry and closes this component
if (keyCode == KeyEvent.VK_ESCAPE) {
cancelEntry();
}
KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e);
// handle undo and redo
if (keyStroke.equals(GemCutterActionKeys.ACCELERATOR_UNDO)) {
if (undoManager.canUndo()) {
undoManager.undo();
textChanged();
}
e.consume();
} else if (keyStroke.equals(GemCutterActionKeys.ACCELERATOR_REDO)) {
if (undoManager.canRedo()) {
undoManager.redo();
textChanged();
}
e.consume();
} else if (keyStroke.equals(GemCutterActionKeys.ACCELERATOR_ARRANGE_GRAPH) ||
keyStroke.equals(GemCutterActionKeys.ACCELERATOR_FIT_TABLETOP) ||
keyStroke.equals(GemCutterActionKeys.ACCELERATOR_NEW)) {
// We have to intercept accelerators for these so that the GemCutter
// doesn't get screwed up when the text field doesn't match the action result.
e.consume();
}
}
});
// ensure the cursor is visible when it moves
addCaretListener(new CaretListener(){
public void caretUpdate(CaretEvent e){
// just ensure the caret is visible
scrollCaretToVisible();
}
});
}
/**
* Returns whether a name is a valid name for this field
* @param name String the name to check for validity
*/
@Override
protected boolean isValidName(String name){
return !disallowedFields.contains(FieldName.make(name)) && LanguageInfo.isValidFieldName(name);
}
/**
* Creates the default implementation of the model to be used at construction if one isn't explicitly given.
* Overridden to return a LetterNumberUnderscorePoundDocument.
* Creation date: (10/29/01 6:50:22 PM)
* @return Document the default model implementation.
*/
@Override
protected Document createDefaultModel() {
return new LetterNumberUnderscorePoundDocument();
}
/**
* Cancel text entry (press "ESC" ..)
*/
@Override
protected void cancelEntry(){
// Revert to the last valid name
setText(getInitialText());
// What to do, what to do..
textCommittedInvalid();
}
/**
* If this has been placed in the tabletop, make sure the caret is visible
*/
void scrollCaretToVisible(){
if (tableTop.getTableTopPanel().isAncestorOf(this)) {
int dotPos = getCaret().getDot();
try {
Rectangle caretRect = modelToView(dotPos);
if (caretRect != null) {
caretRect.width += 1;
Rectangle convertedRect =
SwingUtilities.convertRectangle(this, caretRect, tableTop.getTableTopPanel());
tableTop.getTableTopPanel().scrollRectToVisible(convertedRect);
}
} catch (BadLocationException e) {
// Nowhere to scroll. Oh well.
}
}
}
/**
* Close this window (if not already gone..)
*/
synchronized void closeField(){
// check if we've removed this already
if (!removed) {
removed = true;
tableTop.getTableTopPanel().remove(this);
tableTop.getTableTopPanel().repaint(RecordFieldSelectionTextEditor.this.getBounds());
// we have to do this or else you can just keep typing (..!)
setEnabled(false);
// Update the tabletop for the new gem graph state.
// Among other things, this will ensure arg name disambiguation with respect to the new collector name.
tableTop.updateForGemGraph();
if (textListener != null) {
textListener.closing();
}
}
// trigger a focusLost() on this component if it had focus
tableTop.getTableTopPanel().requestFocus();
}
/**
* Notify that the text of this text field has changed. Called upon insertUpdate() and remove() completion.
* Eg. If the current result is not valid, maybe do something about it (like warn the user somehow..)
*/
@Override
protected void textChanged(){
super.textChanged();
tableTop.resizeForGems();
// ensure the caret is visible
scrollCaretToVisible();
}
/**
* Take appropriate action if the result of the text change is invalid.
*/
@Override
protected void textChangeInvalid(){
// Signal the user.
setForeground(Color.lightGray);
// update the gem field name to display the new text (despite being invalid)
updateGemFieldName(getText());
// set a tooltip saying that the text is invalid
String text = GemCutter.getResourceString("ToolTip_InvalidFieldName");
String[] lines = ToolTipHelpers.splitTextIntoLines(text, 300, getFont(), ((Graphics2D)tableTop.getTableTopPanel().getGraphics()).getFontRenderContext());
text = "<html>" + lines [0];
for (int i = 1; i < lines.length; i++) {
text += "<br>" + lines[i];
}
setToolTipText(text + "</html>");
// update the text field to reflect the new size of the text
updateSize();
}
/**
* Take appropriate action if the result of the text change is valid.
*/
@Override
protected void textChangeValid(){
// do validation checking - paint text colors differently depending on the result
setForeground(Color.black);
// update the gem name to display the new text
updateGemFieldName(getText());
// clear any tooltip saying that the text is invalid
setToolTipText(null);
// update the text field to reflect the new size of the text
updateSize();
}
/**
* Take appropriate action if the text committed is valid.
*/
@Override
protected void textCommittedInvalid(){
revertEdit();
// close this component
closeField();
}
/**
* Take appropriate action if the text committed is valid.
*/
@Override
protected void textCommittedValid(){
commitEdit(getText());
// close this component
closeField();
}
/**
* Update the size of this field.
*/
private void updateSize(){
Insets insets = getInsets();
// The X dimension is based on the size of the text for the name (plus some margins)
FontMetrics fm = getFontMetrics(getFont());
// Calculate width and height
int newWidth = fm.stringWidth(getText()) + insets.right + insets.left + 1;
int newHeight = fm.getHeight();
setSize(new Dimension(newWidth, newHeight));
if (textListener != null) {
textListener.resize(new Dimension(newWidth, newHeight));
}
}
/**
* Update the name of the gem represented by this text field.
* @param newName String the new name for the let gem
*/
private void updateGemFieldName(String newName){
recordFieldSelectionGem.setFieldName(newName);
}
}
/**
* A combo box for editing the field name to select from records of which
* we have some knowledge of the fields contained in the record. It
* provides an option to enter a field of the user's choice, if the record
* is polymorphic.
* @author Neil Corkum
*/
private class RecordFieldSelectionComboEditor extends JComboBox {
private static final long serialVersionUID = -1520958490445008579L;
/** this is set when the combo box is closed - it is used to prevent the
* combo box for duplicating actions when it is closing and committing
* a value. This can happen when multiple events calling for the control
* to close are fired. The combo box cannot be reused once it has been closed.*/
private boolean closed = false;
/**
* This class implements a custom editor for the combo box that is based on a supplied
* RecordFieldSelectionTextEditor. This ensures the combo text editing is
* identical to plain record field text editor.
*/
private class ComboEditor implements ComboBoxEditor, DocumentListener {
/** the actual text editor used by this component*/
private final RecordFieldSelectionTextEditor text;
/**
* Constructs the combo box editor with a specific editor
* @param textEditor the editor to use
*/
public ComboEditor(RecordFieldSelectionTextEditor textEditor) {
text = textEditor;
}
/**
* {@inheritDoc}
*/
public Component getEditorComponent() {
return text;
}
/**
* {@inheritDoc}
*/
public void setItem(Object item) {
text.setText(item.toString());
}
/**
* {@inheritDoc}
*/
public Object getItem() {
return text.getText();
}
/**
* {@inheritDoc}
*/
public void selectAll() { text.selectAll(); }
/**
* {@inheritDoc}
*/
public void addActionListener(ActionListener l) {
text.addActionListener(l);
}
/**
* {@inheritDoc}
*/
public void removeActionListener(ActionListener l) {
text.removeActionListener(l);
}
/**
* {@inheritDoc}
*/
public void insertUpdate(DocumentEvent e) { }
/**
* {@inheritDoc}
*/
public void removeUpdate(DocumentEvent e) { }
/**
* {@inheritDoc}
*/
public void changedUpdate(DocumentEvent e) { }
}
/**
* Constructor for a RecordFieldSelectionComboEditor.
* @param obj objects to put in combo box list. Can include RecordFieldName
* and String (String only used for the "other field" item).
*/
private RecordFieldSelectionComboEditor(Object obj[], RecordFieldSelectionTextEditor textEditor) {
super(obj);
setFont(GemCutterPaintHelper.getTitleFont());
setSelectedItem(oldFieldName);
//if a text editor is supplied, the field is editable
if (textEditor != null) {
setEditable(true);
setEditor(new ComboEditor(textEditor));
}
// popup menu listener to determine when to close editor and commit value
addPopupMenuListener(new PopupMenuListener() {
///** marks whether popup has been canceled.
boolean cancelled = false; // is there a better way to do this?
public void popupMenuCanceled(PopupMenuEvent e) {
// selection canceled; revert to previous value
cancelFieldSelection();
cancelled = true;
}
synchronized public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
// popup menu closing; get rid of editor and commit value if editor not canceled
if (!isClosed()){
setClosed();
tableTop.getTableTopPanel().remove(RecordFieldSelectionComboEditor.this);
if (!cancelled) {
commitFieldSelection();
}
}
}
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
}
});
//listen for when the value is set
addActionListener(new ActionListener() {
synchronized public void actionPerformed(ActionEvent a) {
if (isClosed()) {
setClosed();
tableTop.getTableTopPanel().remove(RecordFieldSelectionComboEditor.this);
commitFieldSelection();
}
}
});
}
/**
* @return true if the combo box has been marked as closed
*/
public boolean isClosed() {
return closed;
}
/**
* mark the combo box as closed
*/
public void setClosed() {
closed = true;
}
/**
* Saves change to the selected field to select.
*/
void commitFieldSelection() {
if (getSelectedIndex() >= 0) {
commitEdit(getSelectedItem().toString());
}
}
/**
* Cancels any changes made to the selected field, and reverts to the
* field name present before creating the editor.
*/
void cancelFieldSelection() {
revertEdit();
}
/**
* Override to ensure that popup to control the visibility of the drop down options for non-polymorphic records
*/
@Override
public void requestFocus() {
if (!isEditable()) {
// always show the popup while the combo box is showing for non polymorphic records.
setPopupVisible(true);
}
super.requestFocus();
}
}
/**
* Constructor for an RecordFieldSelectionGemFieldNameEditor. To make an editor, use the static
* method makeEditor().
* @param recordFieldSelectionGem The RecordFieldSelection Gem this editor is associated with
* @param tableTop A reference to the table top on which the RecordFieldSelection Gem is present
*/
private RecordFieldSelectionGemFieldNameEditor(RecordFieldSelectionGem recordFieldSelectionGem, TableTop tableTop) {
this.recordFieldSelectionGem = recordFieldSelectionGem;
this.tableTop = tableTop;
this.oldFieldName = recordFieldSelectionGem.getFieldNameString();
this.oldFixedState = recordFieldSelectionGem.isFieldFixed();
//update the gem to fixed so the rendering is in fixed mode
this.recordFieldSelectionGem.setFieldFixed(true);
//force the gem to be redrawn
this.recordFieldSelectionGem.setFieldName("");
this.recordFieldSelectionGem.setFieldName(oldFieldName);
}
/**
* Creates an editor for the field name to be selected. This method
* selects the appropriate type of editor - edit box or combo box,
* or nothing if the field may not be edited due to type constraints
* @param recordFieldSelectionGem The RecordFieldSelection Gem this editor is associated with
* @param tableTop A reference to the table top on which the RecordFieldSelection Gem is present
* @return the component for editing the field to be selected, or null if the field cannot be edited.
*/
public static JComponent makeEditor(RecordFieldSelectionGem recordFieldSelectionGem, TableTop tableTop) {
// field editor to return
final JComponent fieldEditor;
final PartInput inputPart = recordFieldSelectionGem.getInputPart();
final TypeExpr inputTypeExpr;
TypeExpr outputTypeExpr = TypeExpr.makeParametricType();
//determine the input type
if (inputPart.isConnected() ) {
inputTypeExpr = inputPart.inferType(tableTop.getTypeCheckInfo()) ;
if (recordFieldSelectionGem.getOutputPart().getConnection() != null) {
outputTypeExpr = recordFieldSelectionGem.getOutputPart().inferType(tableTop.getTypeCheckInfo());
}
} else if (inputPart.isBurnt() && recordFieldSelectionGem.getOutputPart().getConnection() != null) {
TypeExpr outputType = recordFieldSelectionGem.getOutputPart().inferType(tableTop.getTypeCheckInfo());
if (outputType.getArity() > 0) {
TypeExpr[] typePieces = outputType.getTypePieces(1);
inputTypeExpr = typePieces[0];
outputTypeExpr = typePieces[1];
} else {
inputTypeExpr = null;
}
} else {
inputTypeExpr = null;
}
RecordFieldSelectionGemFieldNameEditor editor = new RecordFieldSelectionGemFieldNameEditor(recordFieldSelectionGem, tableTop);
//create the appropriate editor type
if (inputTypeExpr instanceof RecordType) {
RecordType inputType = (RecordType)inputTypeExpr;
Set<FieldName> disallowedFields = new HashSet<FieldName>(inputType.getLacksFieldsSet());
disallowedFields.removeAll(inputType.getHasFieldNames());
List<FieldName> possibleFields = RecordFieldSelectionGem.possibleFieldNames(inputType, outputTypeExpr, tableTop.getCurrentModuleTypeInfo());
if (inputType.isRecordPolymorphic()) {
if (possibleFields.isEmpty()) {
fieldEditor = editor.createTextFieldEditor(disallowedFields);
} else {
fieldEditor = editor.createComboFieldEditor(possibleFields, editor.createTextFieldEditor(disallowedFields));
}
} else {
if (possibleFields.size() == 1) {
//there is only one option - no reason to edit
fieldEditor = null;
} else {
fieldEditor = editor.createComboFieldEditor(possibleFields, null);
}
}
} else {
// input generic/not connected; use text editor
fieldEditor = editor.createTextFieldEditor(Collections.<FieldName>emptySet());
}
return fieldEditor;
}
/**
* Commits an edit and creates an undo action.
*/
private void commitEdit(String newFieldName) {
recordFieldSelectionGem.setFieldFixed(true);
recordFieldSelectionGem.setFieldName(newFieldName);
tableTop.getUndoableEditSupport().postEdit(new UndoableChangeFieldSelectionEdit(tableTop, recordFieldSelectionGem, oldFieldName, oldFixedState));
tableTop.updateForGemGraph();
}
/**
* Reverts any changes to the record field selection gem
*/
private void revertEdit() {
recordFieldSelectionGem.setFieldFixed(oldFixedState);
recordFieldSelectionGem.setFieldName(oldFieldName);
}
/**
* Creates a text field-style editor. This is used when it is appropriate
* to allow the user to input any valid field name for extraction.
*/
private RecordFieldSelectionTextEditor createTextFieldEditor(Set<FieldName> disallowedFields) {
// not connected to record input; display text field for editing field name
RecordFieldSelectionTextEditor fieldEditor = new RecordFieldSelectionTextEditor(disallowedFields);
fieldEditor.setFont(GemCutterPaintHelper.getTitleFont());
// ensure the cursor is visible
fieldEditor.scrollCaretToVisible();
return fieldEditor;
}
/**
* Creates a combo box-style editor. This is used when some or all of the
* fields available in a record to be extracted from are known.
* @param validFieldNames a list of possible field names to display in the combo box
* @param textEditor if the record type is polymorphic this is the editor used to enter new field names, if the record is non polymorphic this should be null.
*/
private JComponent createComboFieldEditor(final List<FieldName> validFieldNames, final RecordFieldSelectionTextEditor textEditor) {
final RecordFieldSelectionComboEditor fieldComboEditor = new RecordFieldSelectionComboEditor(validFieldNames.toArray(), textEditor);
//set the selection to the existing value of it is in the list of values
fieldComboEditor.setSelectedItem(oldFieldName);
final int currentIndex = validFieldNames.indexOf(FieldName.make(oldFieldName));
if (currentIndex >=0 ) {
fieldComboEditor.setSelectedIndex(currentIndex);
}
fieldComboEditor.setSize(fieldComboEditor.getPreferredSize());
//if the text editor is supplied we must catch size changes and closes
if (textEditor != null) {
textEditor.selectAll();
final int sizeDiff = fieldComboEditor.getPreferredSize().width - textEditor.getPreferredSize().width;
//listen for text update and closing events
textEditor.textListener =
new TextListener() {
public void resize(Dimension d) {
fieldComboEditor.setSize(d.width + sizeDiff,fieldComboEditor.getHeight() );
}
public void closing() {
if (!fieldComboEditor.isClosed()){
fieldComboEditor.setClosed();
tableTop.getTableTopPanel().remove(fieldComboEditor);
}
}
};
//listen for when the text value is set value is set
textEditor.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent a) {
if (!fieldComboEditor.isClosed()){
fieldComboEditor.setClosed();
tableTop.getTableTopPanel().remove(fieldComboEditor);
if (!textEditor.isValidName(textEditor.getText())) {
textEditor.cancelEntry();
}
fieldComboEditor.commitFieldSelection();
}
}
});
} else {
//if there is no text field we must watch for focus lost events
//which would would otherwise be taken care of by the text field
fieldComboEditor.addFocusListener(new FocusListener() {
public void focusGained(FocusEvent e) {}
public void focusLost(FocusEvent e) {
if (!fieldComboEditor.isClosed()){
fieldComboEditor.setClosed();
tableTop.getTableTopPanel().remove(fieldComboEditor);
fieldComboEditor.cancelFieldSelection();
}
}
});
//catch the escape key
fieldComboEditor.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {}
public void keyReleased(KeyEvent e) {}
public void keyTyped(KeyEvent e) {
int keyCode = e.getKeyCode();
// pressing "ESC" cancels entry and closes this component
if (keyCode == KeyEvent.VK_ESCAPE) {
if (!fieldComboEditor.isClosed()){
fieldComboEditor.setClosed();
tableTop.getTableTopPanel().remove(fieldComboEditor);
fieldComboEditor.cancelFieldSelection();
}
}
}
});
}
return fieldComboEditor;
}
}