/**
* $Id: mxCellEditor.java,v 1.21 2010-10-28 10:41:05 gaudenz Exp $
* Copyright (c) 2008, Gaudenz Alder
*/
package com.mxgraph.swing.view;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.io.Writer;
import java.util.EventObject;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyledDocument;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTMLWriter;
import javax.swing.text.html.MinimalHTMLWriter;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxUtils;
import com.mxgraph.view.mxCellState;
/**
* To control this editor, use mxGraph.invokesStopCellEditing, mxGraph.
* enterStopsCellEditing and mxGraph.escapeEnabled.
*/
public class mxCellEditor implements mxICellEditor
{
/**
*
*/
private static final String CANCEL_EDITING = "cancel-editing";
/**
*
*/
private static final String INSERT_BREAK = "insert-break";
/**
*
*/
private static final String SUBMIT_TEXT = "submit-text";
/**
*
*/
public static int DEFAULT_MIN_WIDTH = 100;
/**
*
*/
public static int DEFAULT_MIN_HEIGHT = 60;
/**
*
*/
public static double DEFAULT_MINIMUM_EDITOR_SCALE = 1;
/**
*
*/
protected mxGraphComponent graphComponent;
/**
* Defines the minimum scale to be used for the editor. Set this to
* 0 if the font size in the editor
*/
protected double minimumEditorScale = DEFAULT_MINIMUM_EDITOR_SCALE;
/**
*
*/
protected int minimumWidth = DEFAULT_MIN_WIDTH;
/**
*
*/
protected int minimumHeight = DEFAULT_MIN_HEIGHT;
/**
*
*/
protected transient Object editingCell;
/**
*
*/
protected transient EventObject trigger;
/**
*
*/
protected transient JScrollPane scrollPane;
/**
* Holds the editor for plain text editing.
*/
protected transient JTextArea textArea;
/**
* Holds the editor for HTML editing.
*/
protected transient JEditorPane editorPane;
/**
* Specifies if the text content of the HTML body should be extracted
* before and after editing for HTML markup. Default is true.
*/
protected boolean extractHtmlBody = true;
/**
* Specifies if linefeeds should be replaced with BREAKS before editing,
* and BREAKS should be replaced with linefeeds after editing. This
* value is ignored if extractHtmlBody is false. Default is true.
*/
protected boolean replaceLinefeeds = true;
/**
* Specifies if shift ENTER should submit text if enterStopsCellEditing
* is true. Default is false.
*/
protected boolean shiftEnterSubmitsText = false;
/**
*
*/
transient Object editorEnterActionMapKey;
/**
*
*/
transient Object textEnterActionMapKey;
/**
*
*/
transient KeyStroke escapeKeystroke = KeyStroke.getKeyStroke("ESCAPE");
/**
*
*/
transient KeyStroke enterKeystroke = KeyStroke.getKeyStroke("ENTER");
/**
*
*/
transient KeyStroke shiftEnterKeystroke = KeyStroke
.getKeyStroke("shift ENTER");
/**
*
*/
protected AbstractAction cancelEditingAction = new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
stopEditing(true);
}
};
/**
*
*/
protected AbstractAction textSubmitAction = new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
stopEditing(false);
}
};
/**
*
*/
public mxCellEditor(mxGraphComponent graphComponent)
{
this.graphComponent = graphComponent;
// Creates the plain text editor
textArea = new JTextArea();
textArea.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
textArea.setOpaque(false);
// Creates the HTML editor
editorPane = new JEditorPane();
editorPane.setOpaque(false);
editorPane.setContentType("text/html");
// Workaround for inserted linefeeds in HTML markup with
// lines that are longar than 80 chars
editorPane.setEditorKit(new NoLinefeedHtmlEditorKit());
// Creates the scollpane that contains the editor
// FIXME: Cursor not visible when scrolling
scrollPane = new JScrollPane();
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.getViewport().setOpaque(false);
scrollPane.setVisible(false);
scrollPane.setOpaque(false);
// Installs custom actions
editorPane.getActionMap().put(CANCEL_EDITING, cancelEditingAction);
textArea.getActionMap().put(CANCEL_EDITING, cancelEditingAction);
editorPane.getActionMap().put(SUBMIT_TEXT, textSubmitAction);
textArea.getActionMap().put(SUBMIT_TEXT, textSubmitAction);
// Remembers the action map key for the enter keystroke
editorEnterActionMapKey = editorPane.getInputMap().get(enterKeystroke);
textEnterActionMapKey = editorPane.getInputMap().get(enterKeystroke);
}
/**
* Returns replaceHtmlLinefeeds
*/
public boolean isExtractHtmlBody()
{
return extractHtmlBody;
}
/**
* Sets extractHtmlBody
*/
public void setExtractHtmlBody(boolean value)
{
extractHtmlBody = value;
}
/**
* Returns replaceHtmlLinefeeds
*/
public boolean isReplaceHtmlLinefeeds()
{
return replaceLinefeeds;
}
/**
* Sets replaceHtmlLinefeeds
*/
public void setReplaceHtmlLinefeeds(boolean value)
{
replaceLinefeeds = value;
}
/**
* Returns shiftEnterSubmitsText
*/
public boolean isShiftEnterSubmitsText()
{
return shiftEnterSubmitsText;
}
/**
* Sets shiftEnterSubmitsText
*/
public void setShiftEnterSubmitsText(boolean value)
{
shiftEnterSubmitsText = value;
}
/**
* Installs the keyListener in the textArea and editorPane
* for handling the enter keystroke and updating the modified state.
*/
protected void configureActionMaps()
{
InputMap editorInputMap = editorPane.getInputMap();
InputMap textInputMap = textArea.getInputMap();
// Adds handling for the escape key to cancel editing
editorInputMap.put(escapeKeystroke, cancelEditingAction);
textInputMap.put(escapeKeystroke, cancelEditingAction);
// Adds handling for shift-enter and redirects enter to stop editing
if (graphComponent.isEnterStopsCellEditing())
{
editorInputMap.put(shiftEnterKeystroke, editorEnterActionMapKey);
textInputMap.put(shiftEnterKeystroke, textEnterActionMapKey);
editorInputMap.put(enterKeystroke, SUBMIT_TEXT);
textInputMap.put(enterKeystroke, SUBMIT_TEXT);
}
else
{
editorInputMap.put(enterKeystroke, editorEnterActionMapKey);
textInputMap.put(enterKeystroke, textEnterActionMapKey);
if (isShiftEnterSubmitsText())
{
editorInputMap.put(shiftEnterKeystroke, SUBMIT_TEXT);
textInputMap.put(shiftEnterKeystroke, SUBMIT_TEXT);
}
else
{
editorInputMap.remove(shiftEnterKeystroke);
textInputMap.remove(shiftEnterKeystroke);
}
}
}
/**
* Returns the current editor or null if no editing is in progress.
*/
public Component getEditor()
{
if (textArea.getParent() != null)
{
return textArea;
}
else if (editingCell != null)
{
return editorPane;
}
return null;
}
/**
* Returns true if the label bounds of the state should be used for the
* editor.
*/
protected boolean useLabelBounds(mxCellState state)
{
mxIGraphModel model = state.getView().getGraph().getModel();
mxGeometry geometry = model.getGeometry(state.getCell());
return ((geometry != null && geometry.getOffset() != null
&& !geometry.isRelative() && (geometry.getOffset().getX() != 0 || geometry
.getOffset().getY() != 0)) || model.isEdge(state.getCell()));
}
/**
* Returns the bounds to be used for the editor.
*/
public Rectangle getEditorBounds(mxCellState state, double scale)
{
mxIGraphModel model = state.getView().getGraph().getModel();
Rectangle bounds = null;
if (useLabelBounds(state))
{
bounds = state.getLabelBounds().getRectangle();
bounds.height += 10;
}
else
{
bounds = state.getRectangle();
}
// Applies the horizontal and vertical label positions
if (model.isVertex(state.getCell()))
{
String horizontal = mxUtils.getString(state.getStyle(),
mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
if (horizontal.equals(mxConstants.ALIGN_LEFT))
{
bounds.x -= state.getWidth();
}
else if (horizontal.equals(mxConstants.ALIGN_RIGHT))
{
bounds.x += state.getWidth();
}
String vertical = mxUtils.getString(state.getStyle(),
mxConstants.STYLE_VERTICAL_LABEL_POSITION,
mxConstants.ALIGN_MIDDLE);
if (vertical.equals(mxConstants.ALIGN_TOP))
{
bounds.y -= state.getHeight();
}
else if (vertical.equals(mxConstants.ALIGN_BOTTOM))
{
bounds.y += state.getHeight();
}
}
bounds.setSize(
(int) Math.max(bounds.getWidth(),
Math.round(minimumWidth * scale)),
(int) Math.max(bounds.getHeight(),
Math.round(minimumHeight * scale)));
return bounds;
}
/*
* (non-Javadoc)
* @see com.mxgraph.swing.view.mxICellEditor#startEditing(java.lang.Object, java.util.EventObject)
*/
public void startEditing(Object cell, EventObject evt)
{
if (editingCell != null)
{
stopEditing(true);
}
mxCellState state = graphComponent.getGraph().getView().getState(cell);
if (state != null)
{
editingCell = cell;
trigger = evt;
double scale = Math.max(minimumEditorScale, graphComponent
.getGraph().getView().getScale());
scrollPane.setBounds(getEditorBounds(state, scale));
scrollPane.setVisible(true);
String value = getInitialValue(state, evt);
JTextComponent currentEditor = null;
// Configures the style of the in-place editor
if (graphComponent.getGraph().isHtmlLabel(cell))
{
if (isExtractHtmlBody())
{
value = mxUtils.getBodyMarkup(value,
isReplaceHtmlLinefeeds());
}
editorPane.setDocument(mxUtils.createHtmlDocumentObject(
state.getStyle(), scale));
editorPane.setText(value);
// Workaround for wordwrapping in editor pane
// FIXME: Cursor not visible at end of line
JPanel wrapper = new JPanel(new BorderLayout());
wrapper.setOpaque(false);
wrapper.add(editorPane, BorderLayout.CENTER);
scrollPane.setViewportView(wrapper);
currentEditor = editorPane;
}
else
{
textArea.setFont(mxUtils.getFont(state.getStyle(), scale));
Color fontColor = mxUtils.getColor(state.getStyle(),
mxConstants.STYLE_FONTCOLOR, Color.black);
textArea.setForeground(fontColor);
textArea.setText(value);
scrollPane.setViewportView(textArea);
currentEditor = textArea;
}
graphComponent.getGraphControl().add(scrollPane, 0);
if (isHideLabel(state))
{
graphComponent.redraw(state);
}
currentEditor.revalidate();
currentEditor.requestFocusInWindow();
currentEditor.selectAll();
configureActionMaps();
}
}
/**
*
*/
protected boolean isHideLabel(mxCellState state)
{
return true;
}
/*
* (non-Javadoc)
* @see com.mxgraph.swing.view.mxICellEditor#stopEditing(boolean)
*/
public void stopEditing(boolean cancel)
{
if (editingCell != null)
{
scrollPane.transferFocusUpCycle();
Object cell = editingCell;
editingCell = null;
if (!cancel)
{
EventObject trig = trigger;
trigger = null;
graphComponent.labelChanged(cell, getCurrentValue(), trig);
}
else
{
mxCellState state = graphComponent.getGraph().getView()
.getState(cell);
graphComponent.redraw(state);
}
if (scrollPane.getParent() != null)
{
scrollPane.setVisible(false);
scrollPane.getParent().remove(scrollPane);
}
graphComponent.requestFocusInWindow();
}
}
/**
* Gets the initial editing value for the given cell.
*/
protected String getInitialValue(mxCellState state, EventObject trigger)
{
return graphComponent.getEditingValue(state.getCell(), trigger);
}
/**
* Returns the current editing value.
*/
public String getCurrentValue()
{
String result;
if (textArea.getParent() != null)
{
result = textArea.getText();
}
else
{
result = editorPane.getText();
if (isExtractHtmlBody())
{
result = mxUtils
.getBodyMarkup(result, isReplaceHtmlLinefeeds());
}
}
return result;
}
/*
* (non-Javadoc)
* @see com.mxgraph.swing.view.mxICellEditor#getEditingCell()
*/
public Object getEditingCell()
{
return editingCell;
}
/**
* @return the minimumEditorScale
*/
public double getMinimumEditorScale()
{
return minimumEditorScale;
}
/**
* @param minimumEditorScale the minimumEditorScale to set
*/
public void setMinimumEditorScale(double minimumEditorScale)
{
this.minimumEditorScale = minimumEditorScale;
}
/**
* @return the minimumWidth
*/
public int getMinimumWidth()
{
return minimumWidth;
}
/**
* @param minimumWidth the minimumWidth to set
*/
public void setMinimumWidth(int minimumWidth)
{
this.minimumWidth = minimumWidth;
}
/**
* @return the minimumHeight
*/
public int getMinimumHeight()
{
return minimumHeight;
}
/**
* @param minimumHeight the minimumHeight to set
*/
public void setMinimumHeight(int minimumHeight)
{
this.minimumHeight = minimumHeight;
}
/**
* Workaround for inserted linefeeds when getting text from HTML editor.
*/
class NoLinefeedHtmlEditorKit extends HTMLEditorKit
{
public void write(Writer out, Document doc, int pos, int len)
throws IOException, BadLocationException
{
if (doc instanceof HTMLDocument)
{
NoLinefeedHtmlWriter w = new NoLinefeedHtmlWriter(out,
(HTMLDocument) doc, pos, len);
// the default behavior of write() was to setLineLength(80) which resulted in
// the inserting or a CR/LF around the 80ith character in any given
// line. This was not good because if a merge tag was in that range, it would
// insert CR/LF in between the merge tag and then the replacement of
// merge tag with bean values was not working.
w.setLineLength(Integer.MAX_VALUE);
w.write();
}
else if (doc instanceof StyledDocument)
{
MinimalHTMLWriter w = new MinimalHTMLWriter(out,
(StyledDocument) doc, pos, len);
w.write();
}
else
{
super.write(out, doc, pos, len);
}
}
}
/**
* Subclassed to make setLineLength visible for the custom editor kit.
*/
class NoLinefeedHtmlWriter extends HTMLWriter
{
public NoLinefeedHtmlWriter(Writer buf, HTMLDocument doc, int pos,
int len)
{
super(buf, doc, pos, len);
}
protected void setLineLength(int l)
{
super.setLineLength(l);
}
}
}