/*
* 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.Component;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.Point;
import java.awt.RenderingHints;
import java.util.HashMap;
import java.util.Map;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.event.UndoableEditListener;
import ca.gedge.opgraph.ContextualItem;
import ca.gedge.opgraph.InputField;
import ca.gedge.opgraph.OpNode;
import ca.gedge.opgraph.OpNodeListener;
import ca.gedge.opgraph.OutputField;
/**
* A component that visualizes an {@link OpNode}.
*/
public class CanvasNode extends JPanel {
/** The node being displayed */
private OpNode node;
/** A label for the node name */
private CanvasNodeName name;
/** A panel containing all the input fields */
private JPanel inputs;
/** A panel containing all the output fields */
private JPanel outputs;
/** The style used for this component */
private NodeStyle style;
/** Padding used in component */
private final static int PADDING = 4;
/** The selected state */
private boolean selected;
/** A mapping from field to the field component */
private Map<ContextualItem, CanvasNodeField> fields;
/**
* Constructs a component that displays the specified node using a default style.
*
* @param node the node to display
*
* @throws NullPointerException if <code>node</code> is <code>null</code>
*/
public CanvasNode(OpNode node) {
this(node, new NodeStyle());
}
/**
* Constructs a component that displays a node using a particular style.
*
* @param node the node to display
* @param style the style to display this node in
*
* @throws NullPointerException if <code>node</code> is <code>null</code>
*/
public CanvasNode(OpNode node, NodeStyle style) {
super(new GridBagLayout());
// Create components
this.selected = false;
this.name = new CanvasNodeName(node, style);
this.inputs = new JPanel();
this.outputs = new JPanel();
this.fields = new HashMap<ContextualItem, CanvasNodeField>();
// Initialize components
setOpaque(false);
setBorder(null);
inputs.setOpaque(false);
inputs.setLayout(new BoxLayout(inputs, BoxLayout.Y_AXIS));
outputs.setOpaque(false);
outputs.setLayout(new BoxLayout(outputs, BoxLayout.Y_AXIS));
setNode(node);
// Add components to layout
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(PADDING, PADDING, PADDING / 2, PADDING);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 2;
gbc.gridheight = 1;
add(name, gbc);
gbc.insets = new Insets(PADDING, PADDING, PADDING, PADDING);
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.gridy = 1;
gbc.gridwidth = 1;
add(inputs, gbc);
gbc.anchor = GridBagConstraints.NORTHEAST;
gbc.gridx = 1;
add(outputs, gbc);
}
/**
* Gets the canvas field component at the specified point.
*
* @param p the point, in this component's coordinate system
*
* @return the canvas node field component, or <code>null</code> if no
* field component exists at the specified point
*/
public CanvasNodeField getFieldAt(Point p) {
CanvasNodeField field = null;
if(inputs.getBounds().contains(p)) {
p = SwingUtilities.convertPoint(this, p, inputs);
Component comp = inputs.getComponentAt(p);
if(comp instanceof CanvasNodeField)
field = (CanvasNodeField)comp;
} else if(outputs.getBounds().contains(p)) {
p = SwingUtilities.convertPoint(this, p, outputs);
Component comp = outputs.getComponentAt(p);
if(comp instanceof CanvasNodeField)
field = (CanvasNodeField)comp;
}
return field;
}
/**
* Gets a mapping from {@link OpNode} to the respective node
* component that displays that node.
*
* @return the mapping
*/
public Map<ContextualItem, CanvasNodeField> getFieldsMap() {
return fields;
}
/**
* 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);
setBorder(style.NodeBorder);
setBackground(this.style.NodeBackgroundColor);
setForeground(this.style.NodeBorderColor);
// Update children
name.setStyle(this.style);
for(CanvasNodeField fieldComp : fields.values())
fieldComp.setStyle(this.style);
revalidate();
repaint();
}
/**
* Gets the node being displayed by this component.
*
* @return the node
*/
public OpNode getNode() {
return node;
}
/**
* Sets the node being displayed by this component.
*
* @param node the node
*
* @throws NullPointerException if <code>node</code> is <code>null</code>
*/
public void setNode(OpNode node) {
if(node == null) throw new NullPointerException("node cannot be null");
if(node != this.node) {
if(this.node != null)
this.node.removeNodeListener(nodeListener);
this.node = node;
this.node.addNodeListener(nodeListener);
setStyle(NodeStyle.getStyleForNode(node));
super.setToolTipText(node.getDescription());
name.setText(node.getName());
updateFields();
}
}
/**
* Updates the listing of fields in this component.
*/
public void updateFields() {
// Remove old fields, and add new ones
fields.clear();
inputs.removeAll();
outputs.removeAll();
for(InputField field : node.getInputFields())
nodeListener.fieldAdded(node, field);
for(OutputField field : node.getOutputFields())
nodeListener.fieldAdded(node, field);
}
/**
* Set whether or not this node is selected.
*
* @param selected the selected state
*/
public void setSelected(boolean selected) {
this.selected = selected;
revalidate();
repaint();
}
/**
* Gets the selected state of this node.
*
* @return the selected state
*/
public boolean isSelected() {
return selected;
}
/**
* Get the y-coordinate of the separator line that separates the node
* name from the fields.
*
* @return y-coordinate for separator
*/
public int getSeparatorY() {
final Insets insets = getInsets();
if(inputs.getComponentCount() == 0 && outputs.getComponentCount() == 0)
return (getHeight() - insets.bottom - insets.top);
return (name.getHeight() + PADDING / 2 + insets.top);
}
//
// Overrides
//
@Override
public void setBorder(Border border) {
if(border == null)
border = new DefaultNodeBorder();
super.setBorder(border);
}
// TODO export drawing and shape information to NodeStyle
@Override
public void paintComponent(Graphics gfx) {
super.paintComponent(gfx);
Graphics2D g = (Graphics2D)gfx;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
final Insets insets = getInsets();
final int w = getWidth() - insets.left - insets.right;
final int h = getHeight() - insets.top - insets.bottom;
g.translate(insets.left, insets.top);
// Name area background
final int separatorY = getSeparatorY();
Paint namePaint = new GradientPaint(0, 0, style.NodeNameTopColor, 0, separatorY, style.NodeNameBottomColor);
g.setPaint(namePaint);
g.fillRect(0, 0, w, separatorY);
g.setColor(style.NodeBackgroundColor);
g.fillRect(0, separatorY, w, h - separatorY);
// Node name separator
g.setColor(style.NodeBorderColor);
g.drawLine(0, separatorY, w - 1, separatorY);
// Translate back
g.translate(-insets.left, -insets.top);
}
//
// OpNpdeListener
//
private final OpNodeListener nodeListener = new OpNodeListener() {
@Override
public void fieldRemoved(OpNode node, OutputField field) {
final CanvasNodeField fieldComp = fields.get(field);
outputs.remove(fieldComp);
fields.remove(field);
revalidate();
repaint();
}
@Override
public void fieldRemoved(OpNode node, InputField field) {
final CanvasNodeField fieldComp = fields.get(field);
inputs.remove(fieldComp);
fields.remove(field);
revalidate();
repaint();
}
@Override
public void fieldAdded(OpNode node, OutputField field) {
final CanvasNodeField fieldComp = new CanvasNodeField(field);
fieldComp.setStyle(style);
outputs.add(fieldComp);
fields.put(field, fieldComp);
revalidate();
repaint();
}
@Override
public void fieldAdded(OpNode node, InputField field) {
if(field != OpNode.ENABLED_FIELD || style.ShowEnabledField) {
final CanvasNodeField fieldComp = new CanvasNodeField(field);
fieldComp.setStyle(style);
inputs.add(fieldComp);
fields.put(field, fieldComp);
revalidate();
repaint();
}
}
@Override
public void nodePropertyChanged(String propertyName, Object oldValue, Object newValue) {
if(propertyName.equals(OpNode.NAME_PROPERTY)) {
revalidate();
repaint();
}
}
};
//
// UndoableEdit support
//
/**
* Adds an undoable edit listener to this component.
*
* @param listener the listener to add
*/
public void addUndoableEditListener(UndoableEditListener listener) {
name.addUndoableEditListener(listener);
}
/**
* Removes an undoable edit listener from this component.
*
* @param listener the listener to remove
*/
public void removeUndoableEditListener(UndoableEditListener listener) {
name.removeUndoableEditListener(listener);
}
}