/*
* Copyright (c) 2008, SQL Power Group Inc.
*
* This file is part of Wabit.
*
* Wabit is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* Wabit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.sqlpower.swingui.querypen;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JEditorPane;
import javax.swing.UIManager;
import javax.swing.border.LineBorder;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import org.apache.log4j.Logger;
import ca.sqlpower.swingui.querypen.event.ExtendedStyledTextEventHandler;
import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PCanvas;
import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.util.PPaintContext;
import edu.umd.cs.piccolox.event.PStyledTextEventHandler;
import edu.umd.cs.piccolox.nodes.PStyledText;
/**
* This class adds an {@link ExtendedStyledTextEventHandler} to the PStyledText
* to allow editing of the text on clicking the text. A listener can be added
* to this object to listen for when editing of the text starts and stops.
* <p>
* The JTextComponent that is the editing component of this text area is also
* attached to this extended PStyledText.
*/
public class EditablePStyledText extends PStyledText {
private static final Logger logger = Logger.getLogger(EditablePStyledText.class);
/**
* The editor pane shown when the text is clicked. The text entered into this
* pane will modify the text shown in this PStyledText.
*/
private final JEditorPane editorPane;
/**
* An attribute set that contains the font family for lists. This will set the
* font of this PStyledText to be a more normal looking font within the app.
*/
private final SimpleAttributeSet attributeSet;
/**
* This handles the mouse click on the text and shows the editor if the mouse
* has actually clicked on the text.
*/
private final ExtendedStyledTextEventHandler styledTextEventHandler;
/**
* This listener will set the text of this PStyledText and hide the editor
* pane when the editor pane loses focus (ie: clicked away from the editor).
*/
private FocusListener editorFocusListener = new FocusListener() {
public void focusLost(FocusEvent e) {
styledTextEventHandler.stopEditing();
}
public void focusGained(FocusEvent e) {
//do nothing
}
};
/**
* The document shared between the editor pane and this PStyledText object.
* This will contain the shared text between the two objects and has
* the attribute set attached to it.
*/
private DefaultStyledDocument doc;
/**
* A list of listeners that fire when this styled text's text is starting or
* stopping from being in an editable state.
*/
private List<EditStyledTextListener> editingListeners;
/**
* Tells the paint method if we should show a border or not. The border will show
* when the user has moused over the component.
*/
private boolean showHoverBorder;
/**
* This listener will place an empty string in the where text field if there is no
* value in the where field. This is done to give the where field a minimum size
* since the field is always constrained to the text inside it.
*/
private EditStyledTextListener emptyListener = new EditStyledTextListener() {
public void editingStopping() {
fillWithSpaces();
}
public void editingStarting() {
editorPane.setText(editorPane.getText().trim());
}
};
/**
* This is the minimum number of characters that will space out the text field. If
* there are fewer than this many characters in the text field white space will be
* appended to the view portion to give users a larger field to click on. This is
* used instead of defining a minimum width because Piccolo does not have minimum
* or maximum width values defined internally and the PStyledText recomputes the
* layout every time the bounds are set.
*/
protected final int minCharCountSize;
/**
* This is the canvas this PStyledText node is placed on. The canvas is used to place
* and position an editor for the PStyledText node so the user can update the text.
*/
private final PCanvas canvas;
/**
* This listener is placed on the camera to listen to changes in the
* camera's position and move the editor pane accordingly.
* <p>
* This is done in support of bug 1720 where the editor pane was not moving
* when the canvas was being scrolled. The editor pane is placed on the
* canvas to assure the editor comes in front of all of the layers and is at
* a size that is human readable. This code, taken from the
* {@link PStyledTextEventHandler#startEditing(PInputEvent, PStyledText)}
* method, continually repositions the editor as the camera moves.
*/
private final PropertyChangeListener cameraViewChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
logger.debug("Property changed for scroll, type: " + evt.getPropertyName());
Insets pInsets = getInsets();
Point2D nodePt = new Point2D.Double(getX()+pInsets.left,getY()+pInsets.top);
localToGlobal(nodePt);
canvas.getCamera().viewToLocal(nodePt);
Insets bInsets = editorPane.getBorder().getBorderInsets(editorPane);
editorPane.setLocation((int)nodePt.getX()-bInsets.left,(int)nodePt.getY()-bInsets.top);
}
};
protected final String startingText;
public EditablePStyledText(QueryPen queryPen, PCanvas canvas) {
this("", queryPen, canvas);
}
public EditablePStyledText(String startingText, QueryPen queryPen, PCanvas canvas) {
this(startingText, queryPen, canvas, 0);
}
public EditablePStyledText(String startingText, QueryPen queryPen, final PCanvas canvas, int minCharCountSize) {
this.startingText = startingText;
this.minCharCountSize = minCharCountSize;
this.canvas = canvas;
editorPane = new JEditorPane();
editingListeners = new ArrayList<EditStyledTextListener>();
doc = new DefaultStyledDocument();
attributeSet = new SimpleAttributeSet();
attributeSet.addAttribute(StyleConstants.FontFamily, UIManager.getFont("List.font").getFamily());
editorPane.setDocument(doc);
editorPane.setBorder(new LineBorder(editorPane.getForeground()));
editorPane.setText(startingText);
doc.setParagraphAttributes(0, editorPane.getText().length(), attributeSet, false);
setDocument(editorPane.getDocument());
addEditStyledTextListener(emptyListener);
canvas.getCamera().addPropertyChangeListener(PCamera.PROPERTY_VIEW_TRANSFORM, cameraViewChangeListener);
styledTextEventHandler = new ExtendedStyledTextEventHandler(queryPen, canvas, editorPane) {
@Override
public void startEditing(PInputEvent event, PStyledText text) {
for (EditStyledTextListener l : editingListeners) {
l.editingStarting();
}
super.startEditing(event, text);
}
@Override
public void stopEditing() {
editorPane.setText(editorPane.getText().replaceAll("\n", "").trim());
fillWithSpaces();
syncWithDocument();
for (EditStyledTextListener l : editingListeners) {
l.editingStopping();
}
super.stopEditing();
logger.debug("Editing stopped.");
}
};
addInputEventListener(styledTextEventHandler);
editorPane.addKeyListener(new KeyListener() {
public void keyTyped(KeyEvent e) {
//Do nothing
}
public void keyReleased(KeyEvent e) {
//Do nothing
}
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
styledTextEventHandler.stopEditing();
}
}
});
editorPane.addFocusListener(editorFocusListener);
addInputEventListener(new PBasicInputEventHandler() {
@Override
public void mouseEntered(PInputEvent event) {
showHoverBorder = true;
repaint();
}
@Override
public void mouseExited(PInputEvent event) {
showHoverBorder = false;
repaint();
}
});
}
@Override
protected void paint(PPaintContext paintContext) {
super.paint(paintContext);
Graphics2D g = paintContext.getGraphics();
Paint oldPaint = g.getPaint();
Stroke oldStroke = g.getStroke();
g.setStroke(new BasicStroke(1));
g.setPaint(Color.GRAY);
if (showHoverBorder) {
g.drawRect((int) getBounds().getX(), (int) getBounds().getY(), (int) getBounds().getWidth() , (int) getBounds().getHeight());
}
g.setStroke(oldStroke);
g.setPaint(oldPaint);
}
/**
* This will allow classes extending this class to get the focus listener that
* will stop the edit when focus is lost. Other classes may want to define different
* behaviour when focus is lost.
*/
protected FocusListener getEditorFocusListener() {
return editorFocusListener;
}
/**
* Allows classes extending this class to get the styledTextEventHandler that
* handles text replacements for this class.
*/
protected ExtendedStyledTextEventHandler getStyledTextEventHandler() {
return styledTextEventHandler;
}
public void addEditStyledTextListener(EditStyledTextListener l) {
editingListeners.add(l);
}
public void removeEditStyledTextListener(EditStyledTextListener l) {
editingListeners.add(l);
}
public JEditorPane getEditorPane() {
return editorPane;
}
private void fillWithSpaces() {
if (editorPane.getText() == null || editorPane.getText().trim().equals("") ) {
editorPane.setText(startingText);
syncWithDocument();
} else if ( editorPane.getText().length() < minCharCountSize) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < minCharCountSize - editorPane.getText().length(); i++) {
sb.append(" ");
}
editorPane.setText(editorPane.getText() + sb.toString());
syncWithDocument();
}
}
}