/*
* 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.
*/
/*
* RecordFieldRenameEditor.java
* Creation date: Oct 4, 2007
* By: Jennifer Chen
*/
package org.openquark.gems.client;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.List;
import java.util.Map;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
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.gems.client.Gem.PartInput;
import org.openquark.gems.client.utilities.ExtendedUndoManager;
/**
* This class is responsible for displaying the correct field name editor for a given Record Creation Gem.
* The editor is specific to the field which it is activated on
*
* @author Jennifer Chen
*/
public class RecordFieldRenameEditor extends EditableIdentifierNameField {
private static final long serialVersionUID = -5205246630972127102L;
/** The RecordCreationGem Gem for this editor */
private RecordCreationGem rcGem;
/** The original field name before edit */
private final String oldFieldName;
/** The list of existing names which the new name cannot overlap with*/
private final List<String> restrictedFieldNames;
/** Saves the previous state of field name and inputs before the edit */
private final Map<String, PartInput> fieldToInputMap;
/** TableTop reference */
private final TableTop tableTop;
/** The undo manager for this text field */
private ExtendedUndoManager undoManager;
/** Keeps track of whether this component has been removed from the tableTop */
private boolean removed = false;
/** Index of the field which we are modifying */
private final int fieldIndex;
public RecordFieldRenameEditor(RecordCreationGem rcGem, FieldName fieldToRename, TableTop tabletop) {
super();
this.rcGem = rcGem;
this.tableTop = tabletop;
this.fieldToInputMap = rcGem.getFieldNameToInputMap();
this.oldFieldName = fieldToRename.getCalSourceForm();
this.restrictedFieldNames = rcGem.getCopyOfFieldsList();
this.fieldIndex = restrictedFieldNames.indexOf(oldFieldName);
// set the initial text
setInitialText(oldFieldName);
setText(oldFieldName);
// 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) {
// Note: when user presses Enter, an actionPerformed() in EditableIdentifierNameField will cause a
// commitText(), then the focusLost event will also commitText(). The extra commit is ok given that
// the extra UndoEdit posted is removed. Therefore in the UndoableModifyRecordFieldEdit, we need to
// implement the replaceEdit() method so there is only 1 edit for the rename action.
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();
}
});
}
@Override
protected boolean isValidName(String name) {
// String is not a valid field name
if (!LanguageInfo.isValidFieldName(name)) {
return false;
}
// Determine if the current input is duplicate to an existing name
for (String tabooName : restrictedFieldNames) {
if (tabooName.equals(name) && !name.equals(oldFieldName)) {
return false;
}
}
return true;
}
/**
* Notify that the text of this text field has changed. Called upon insertUpdate() and remove() completion.
*/
@Override
protected void textChanged() {
super.textChanged();
tableTop.resizeForGems();
// ensure the caret is visible
scrollCaretToVisible();
}
@Override
protected void textChangeInvalid() {
// Signal the user.
setForeground(Color.lightGray);
// Update the gem field name to display the new text (despite being invalid)
updateFieldName(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();
}
@Override
protected void textChangeValid() {
setForeground(Color.black);
// update the gem name to display the new text
updateFieldName(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();
}
@Override
protected void textCommittedInvalid() {
// revert changes
rcGem.renameRecordField(fieldIndex, oldFieldName);
// close this component
closeField();
}
@Override
protected void textCommittedValid() {
// Update the gem name.
String committedText = getText();
updateFieldName(committedText);
// close this component
closeField();
// Notify the undo manager of the name change, if any
tableTop.updateForGemGraph();
tableTop.getUndoableEditSupport().postEdit(new UndoableModifyRecordFieldEdit(rcGem, fieldToInputMap));
}
/**
* @param newFieldName the new name for the field
*/
private void updateFieldName(String newFieldName) {
rcGem.renameRecordField(fieldIndex, newFieldName);
}
/**
* 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));
}
/**
* Creates the default implementation of the model to be used at construction if one isn't explicitly given.
* Overridden to return a LetterNumberUnderscorePoundDocument.
* @return Document the default model implementation.
*/
@Override
protected Document createDefaultModel() {
//Underscores are allowed for field names
return new LetterNumberUnderscorePoundDocument();
}
/**
* Cancel text entry (press "ESC" ..)
*/
@Override
protected void cancelEntry() {
// Revert to the last valid name
setText(getInitialText());
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() {
if (!removed) {
removed = true;
tableTop.getTableTopPanel().remove(this);
tableTop.getTableTopPanel().repaint(RecordFieldRenameEditor.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();
}
// trigger a focusLost() on this component if it had focus
tableTop.getTableTopPanel().requestFocus();
}
}