/*
* Copyright (C) 2012 Jason Gedge <http://www.gedge.ca>
*
* This file is part of the OpGraph project.
*
* This program 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.
*
* This program 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.gedge.opgraph.app.components.canvas;
import java.awt.BasicStroke;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Ellipse2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JComponent;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import ca.gedge.opgraph.ContextualItem;
import ca.gedge.opgraph.InputField;
import ca.gedge.opgraph.OpNode;
import ca.gedge.opgraph.OutputField;
import ca.gedge.opgraph.app.components.DoubleClickableTextField;
/**
* A component that displays either an {@link InputField} or an {@link OutputField}.
*/
public class CanvasNodeField extends JComponent {
/**
* State for link anchor points on this field.
*/
public static enum AnchorFillState {
/** No fill state */
NONE,
/** Link is attached to this field */
LINK,
/** A default value is attached to this field */
DEFAULT,
/** This field is published */
PUBLISHED,
}
/** Stroke we use to show an optional input field */
private final static BasicStroke optionalFieldStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 1, new float[]{1}, 0);
/** The input/output field being displayed */
private ContextualItem field;
/** The field's anchoring point for an link */
private Ellipse2D anchor;
/** The style used for this component */
private NodeStyle style;
/** The color used to fill this anchor */
private AnchorFillState anchorFillState;
/** The field's name */
private FieldName name;
/**
* Extension of {@link DoubleClickableTextField} that modifies the current
* field's key whenever the text changes.
*/
private class FieldName extends JTextField {
/** Double-click support */
private DoubleClickableTextField doubleClickSupport;
/** The field this text field displays */
private ContextualItem field;
/** The default font */
private final Font defaultFont;
public FieldName() {
this.doubleClickSupport = new DoubleClickableTextField(this);
this.defaultFont = getFont();
setBorder(new EmptyBorder(2, 5, 2, 0));
this.doubleClickSupport.addPropertyChangeListener(DoubleClickableTextField.TEXT_PROPERTY, textListener);
}
public void setField(ContextualItem field) {
this.field = field;
super.setText(field == null ? "" : field.getKey());
super.setToolTipText(field == null ? "" : field.getDescription());
if(field instanceof InputField) {
setHorizontalAlignment(SwingConstants.LEFT);
setEditable( !((InputField)field).isFixed() );
setFont(((InputField)field).isOptional() ? defaultFont.deriveFont(Font.ITALIC) : defaultFont);
} else if(field instanceof OutputField) {
setHorizontalAlignment(SwingConstants.RIGHT);
setEditable( !((OutputField)field).isFixed() );
setFont(defaultFont);
} else {
setEditable(true);
setFont(defaultFont);
}
}
private PropertyChangeListener textListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent e) {
if(field != null)
field.setKey((String)e.getNewValue());
}
};
}
/**
* Constructs a component that displays the given field.
*
* @param field the field
*
* @throws NullPointerException if specified field is <code>null</code>
*/
public CanvasNodeField(ContextualItem field) {
this.name = new FieldName();
this.anchor = new Ellipse2D.Double();
this.anchorFillState = AnchorFillState.NONE;
setLayout(null);
setFont(UIManager.getLookAndFeelDefaults().getFont("Label.font"));
setField(field);
setOpaque(false);
addMouseListener(mouseAdapter);
addMouseMotionListener(mouseMotionAdapter);
add(name);
}
/**
* Get the link anchoring area for this field.
*
* @return the anchor
*/
public Ellipse2D getAnchor() {
return (Ellipse2D)anchor.clone();
}
/**
* Sets the state used for the anchor fill.
*
* If the given state is {@link AnchorFillState#DEFAULT}, and the current
* state is {@link AnchorFillState#LINK}, then the change does not occur.
*
* @param anchorFillState the fill state
*/
public void setAnchorFillState(AnchorFillState anchorFillState) {
if(this.anchorFillState != anchorFillState) {
this.anchorFillState = anchorFillState;
repaint();
}
}
/**
* Sets the anchor fill state to a specified state, but only if the
* current state is not {@link AnchorFillState#LINK}.
*
* @param anchorFillState the fill state
*/
public void updateAnchorFillState(AnchorFillState anchorFillState) {
if(this.anchorFillState != AnchorFillState.LINK)
setAnchorFillState(anchorFillState);
}
/**
* Gets the style used for this node.
*
* @return the node style
*/
public NodeStyle getStyle() {
return style;
}
/**
* Sets the style used for this node.
*
* @param style the node style
*/
public void setStyle(NodeStyle style) {
this.style = (style == null ? new NodeStyle() : style);
if(!style.ShowEnabledField && field == OpNode.ENABLED_FIELD) {
if(getParent() != null)
getParent().remove(this);
} else {
revalidate();
}
}
/**
* Gets the field being displayed by this component.
*
* @return the field
*/
public ContextualItem getField() {
return field;
}
/**
* Sets the field being displayed by this component.
*
* @param field the field
*
* @throws NullPointerException if specified field is <code>null</code>
*/
public void setField(ContextualItem field) {
if(field == null) throw new NullPointerException("field cannot be null");
if(field != this.field) {
this.field = field;
name.setField(field);
setToolTipText(field.getDescription());
revalidate();
}
}
//
// Overrides
//
@Override
public void setBounds(int newX, int newY, int newW, int newH) {
super.setBounds(newX, newY, newW, newH);
// Everything else based off of insets
final Insets insets = getInsets();
newW -= insets.left + insets.right + 1;
newH -= insets.top + insets.bottom + 1;
// Update anchor based on whether or not this is an input/output field
final int pad = newH - 1;
double x = insets.left;
double y = insets.top + (pad + 5.0) / 6;
double w = (2.0*pad) / 3;
double h = (2.0*pad) / 3;
if(field instanceof InputField) {
name.setBounds((int)(x + w), insets.top, (int)(newW - 2*x - w), newH);
} else if(field instanceof OutputField) {
x += newW - w;
name.setBounds(insets.left, insets.right, (int)(newW - w - 3), newH);
}
anchor.setFrame(x, y, w, h);
}
@Override
public Dimension getPreferredSize() {
final Dimension textPref = name.getPreferredSize();
final int anchorSize = textPref.height;
return new Dimension(textPref.width + anchorSize + 2, textPref.height);
}
@Override
protected void paintComponent(Graphics gfx) {
super.paintComponent(gfx);
Graphics2D g = (Graphics2D)gfx;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
switch(anchorFillState) {
case LINK:
g.setColor(style.AnchorLinkFillColor);
g.fill(anchor);
break;
case DEFAULT:
g.setColor(style.AnchorDefaultFillColor);
g.fill(anchor);
break;
case PUBLISHED:
g.setColor(style.AnchorPublishedFillColor);
g.fill(anchor);
break;
case NONE:
break;
}
g.setColor(style.FieldsTextColor);
if((field instanceof InputField) && ((InputField)field).isOptional()) {
final Stroke oldStroke = g.getStroke();
g.setStroke(optionalFieldStroke);
g.draw(anchor);
g.setStroke(oldStroke);
} else {
g.draw(anchor);
}
}
//
// MouseAdapter
//
private final MouseAdapter mouseAdapter = new MouseAdapter() {
@Override
public void mouseExited(MouseEvent e) {
Component parentCanvas = SwingUtilities.getAncestorOfClass(GraphCanvas.class, CanvasNodeField.this);
if(parentCanvas != null)
parentCanvas.setCursor(null);
}
@Override
public void mousePressed(MouseEvent e) {
GraphCanvas parentCanvas = (GraphCanvas)SwingUtilities.getAncestorOfClass(GraphCanvas.class, CanvasNodeField.this);
if(parentCanvas != null) {
if(anchor.contains(e.getPoint()))
parentCanvas.startLinkDrag(CanvasNodeField.this);
}
}
@Override
public void mouseReleased(MouseEvent e) {
GraphCanvas parentCanvas = (GraphCanvas)SwingUtilities.getAncestorOfClass(GraphCanvas.class, CanvasNodeField.this);
if(parentCanvas != null)
parentCanvas.endLinkDrag(SwingUtilities.convertPoint(CanvasNodeField.this, e.getPoint(), parentCanvas));
}
};
//
// MouseMotionAdapter
//
private final MouseMotionAdapter mouseMotionAdapter = new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
GraphCanvas parentCanvas = (GraphCanvas)SwingUtilities.getAncestorOfClass(GraphCanvas.class, CanvasNodeField.this);
if(parentCanvas != null)
parentCanvas.updateLinkDrag(SwingUtilities.convertPoint(CanvasNodeField.this, e.getPoint(), parentCanvas));
}
@Override
public void mouseMoved(MouseEvent e) {
Component parentCanvas = SwingUtilities.getAncestorOfClass(GraphCanvas.class, CanvasNodeField.this);
if(parentCanvas != null) {
if(anchor.contains(e.getPoint())) {
parentCanvas.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
} else {
parentCanvas.setCursor(null);
}
}
}
};
}