/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.osedu.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package tufts.vue;
import tufts.vue.shape.*;
import java.lang.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import edu.tufts.vue.ontology.ui.OntologySelectionEvent;
/**
* VueTool for creating LWNodes. Methods for creating default new nodes based on
* tool states, and for handling the drag-create of new nodes.
*/
public class NodeTool extends VueTool
implements VueConstants//, LWEditor
{
private static NodeTool singleton = null;
/** the contextual tool panel **/
private static NodeToolPanel sNodeToolPanel;
/** this constructed called via VueResources.properties init */
public NodeTool()
{
super();
if (singleton != null)
new Throwable("Warning: mulitple instances of " + this).printStackTrace();
singleton = this;
VueToolUtils.setToolProperties(this,"nodeTool");
}
/*
final public Object getPropertyKey() { return LWKey.Shape; }
public Object produceValue() {
// this not actually used right now.
return getActiveSubTool().getShapeInstance();
}
/** LWPropertyProducer impl: load the currently selected tool to the one with given shape
public void displayValue(Object shape) {
// Find the sub-tool with the matching shape, then load it's button icon images
// into the displayed selection icon
if (shape == null)
return;
Enumeration e = getSubToolIDs().elements();
while (e.hasMoreElements()) {
String id = (String) e.nextElement();
SubTool subtool = (SubTool) getSubTool(id);
if (subtool.isShape((RectangularShape) shape)) {
((PaletteButton)mLinkedButton).setPropertiesFromItem(subtool.mLinkedButton);
// call super.setSelectedSubTool to avoid firing the shape setters
// as we're only LOADING the value here.
super.setSelectedSubTool(subtool);
break;
}
}
}
*/
/** return the singleton instance of this class */
public static NodeTool getTool()
{
if (singleton == null) {
// new Throwable("Warning: NodeTool.getTool: class not initialized by VUE").printStackTrace();
//throw new IllegalStateException("NodeTool.getTool: class not initialized by VUE");
new NodeTool();
}
return singleton;
}
private static final Object LOCK = new Object();
// todo: promote a generic static to VueTool that handles the lock,
// and calls subclass factory for creating tool panel
static NodeToolPanel getNodeToolPanel()
{
if (DEBUG.Enabled) tufts.Util.printStackTrace("deprecated");
synchronized (LOCK) {
if (sNodeToolPanel == null) {
sNodeToolPanel = new NodeToolPanel();
}
}
return sNodeToolPanel;
}
public void setSelectedSubTool(VueTool tool) {
super.setSelectedSubTool(tool);
if (VUE.getSelection().size() > 0) {
SubTool shapeTool = (SubTool) tool;
shapeTool.getShapeSetterAction().fire(this);
}
}
public JPanel getContextualPanel() {
return getNodeToolPanel();
}
public static NodeTool.SubTool getActiveSubTool()
{
return (SubTool) getTool().getSelectedSubTool();
}
@Override
public Class getSelectionType() { return LWNode.class; }
public boolean supportsSelection() { return true; }
//private java.awt.geom.RectangularShape currentShape = new tufts.vue.shape.RectangularPoly2D(4);
private java.awt.geom.RectangularShape currentShape;
// todo: if we had a DrawContext here instead of just the graphics,
// we could query it for zoom factor (passed in from mapViewer) so the stroke width would
// look right.
public void drawSelector(java.awt.Graphics2D g, java.awt.Rectangle r)
{
//g.setXORMode(java.awt.Color.blue);
g.draw(r);
currentShape = getActiveSubTool().getShape();
currentShape.setFrame(r);
g.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
//g.setColor(COLOR_NODE_DEFAULT);
//g.fill(currentShape);
//g.setColor(COLOR_BORDER);
//g.setStroke(STROKE_ONE); // todo: scale based on the scale in the GC affine transform
//g.setStroke(new BasicStroke(2f * (float) g.getTransform().getScaleX())); // GC not scaled while drawing selector...
g.setColor(COLOR_SELECTION);
g.draw(currentShape);
/*
if (VueUtil.isMacPlatform()) // Mac 1.4.1 handles XOR differently than PC
g.draw(currentShape);
else
g.fill(currentShape);
*/
}
/*public boolean handleSelectorRelease(MapMouseEvent e)
{
LWNode node = createNode(VueResources.getString("newnode.html"), true);
node.setAutoSized(false);
node.setFrame(e.getMapSelectorBox());
MapViewer viewer = e.getViewer();
viewer.getFocal().addChild(node);
VUE.getUndoManager().mark("New Node");
VUE.getSelection().setTo(node);
viewer.activateLabelEdit(node);
return true;
}*/
/*
public void handleSelectorRelease(java.awt.geom.Rectangle2D mapRect)
{
LWNode node = createNode();
node.setAutoSized(false);
node.setFrame(mapRect);
VUE.getActiveMap().addNode(node);
VUE.getSelection().setTo(node);
VUE.getActiveViewer().activateLabelEdit(node);
}
*/
/**
* Create a new node with the current default properties
* @param name the name for the new node, can be null
* @return the newly constructed node
*/
/*public static LWNode createNode(String name) {
return createNode(name, false);
}
*/
/** @return a new default node with no label */
/*public static LWNode createNode() {
return createNode(null);
}*/
/** @return a new default node with the default new node label */
/*public static LWNode createNewNode() {
return createNode(VueResources.getString("newnode.html"));
}*/
/*
public static LWNode initAsTextNode(LWNode node)
{
node.setIsTextNode(true);
node.setAutoSized(true);
node.setShape(new java.awt.geom.Rectangle2D.Float());
node.setStrokeWidth(0f);
//node.setFillColor(COLOR_TRANSPARENT);
node.setFont(LWNode.DEFAULT_TEXT_FONT);
return node;
}*/
/*
public static LWNode buildTextNode(String text) {
LWNode node = new LWNode();
initAsTextNode(node);
node.setLabel(text);
return node;
}
*/
/**
* Create a new node with the current default properties.
* @param name the name for the new node, can be null
* @param useToolShape if true, shape of node is shape of node tool, otherwise, shape in contextual toolbar
* @return the newly constructed node
*/
/*public static LWNode createNode(String name, boolean useToolShape)
{
LWNode node = new LWNode(name, getActiveSubTool().getShapeInstance());*/
/*
VueBeanState state = getNodeToolPanel().getCurrentState();
if (state != null) {
if (useToolShape) {
// clear out shape if there is one as node already had it's
// shape set based on state of the node tool
state.removeProperty(LWKey.Shape.name);
}
state.applyState(node);
}
node.setAutoSized(true);
*/
/*return node;
}*/
/** For creating text nodes through the tools and on the map: will adjust text size for current zoom level */
/* public static LWNode createTextNode(String text)
{
LWNode node = buildTextNode(text);*/
/*
VueBeanState state = TextTool.getTextToolPanel().getCreationStyle();
if (state != null)
state.applyState(node);
*/
/*if (VUE.getActiveViewer() != null) {
// Okay, for now this completely overrides the font size from the text toolbar...
final Font font = node.getFont();
final float curZoom = (float) VUE.getActiveViewer().getZoomFactor();
final int minSize = LWNode.DEFAULT_TEXT_FONT.getSize();
//if (curZoom * font.getSize() < minSize)
node.setFont(font.deriveFont(minSize / curZoom));
}
return node;
}*/
/** @return an array of actions, with icon set, that will set the shape of selected
* LWNodes */
public Action[] getShapeSetterActions() {
Action[] actions = new Action[getSubToolIDs().size()];
Enumeration e = getSubToolIDs().elements();
int i = 0;
while (e.hasMoreElements()) {
String id = (String) e.nextElement();
NodeTool.SubTool nt = (NodeTool.SubTool) getSubTool(id);
actions[i++] = nt.getShapeSetterAction();
}
return actions;
}
/** @return an array of standard supported shapes for nodes */
public Object[] getAllShapeValues() {
Object[] values = new Object[getSubToolIDs().size()];
Enumeration e = getSubToolIDs().elements();
int i = 0;
while (e.hasMoreElements()) {
String id = (String) e.nextElement();
NodeTool.SubTool nt = (NodeTool.SubTool) getSubTool(id);
values[i++] = nt.getShape();
}
return values;
}
public static class NodeModeTool extends VueTool
{
private LWNode creationNode = new LWNode("");
//private LWNode creationNode = new LWNode("New Node");
public NodeModeTool()
{
super();
creationNode.setAutoSized(false);
setActiveWhileDownKeyCode(KeyEvent.VK_X);
}
@Override
public Class getSelectionType() { return LWNode.class; }
@Override
public boolean handleMousePressed(MapMouseEvent e) {
//out("MOUSE PRESSED");
// Get the creation node ready in case we're about to do a drag:
// (todo: VueTool.handleDragBegin)
EditorManager.applyCurrentProperties(creationNode);
return false;
}
@Override
public boolean handleSelectorRelease(MapMouseEvent e)
{
//LWNode node = createNode(VueResources.getString("newnode.html"), true);
final LWNode node = (LWNode) creationNode.duplicate();
node.setAutoSized(false);
node.setFrame(e.getMapSelectorBox());
node.setLabel(VueResources.getString("newnode.html"));
MapViewer viewer = e.getViewer();
viewer.getFocal().addChild(node);
VUE.getUndoManager().mark("New Node");
VUE.getSelection().setTo(node);
viewer.activateLabelEdit(node);
//creationNodeCurrent = false;
return true;
}
@Override
public void drawSelector(DrawContext dc, java.awt.Rectangle r)
{
dc.g.draw(r);
// TODO: doesn't handle zoom
// Also, handleSelectorRelease can now just dupe the creationNode
creationNode.setFrame(r);
creationNode.draw(dc);
}
/** @return a new node with the default VUE new-node label, initialized with a style from the current editor property states */
public static LWNode createNewNode() {
return createNewNode(VueResources.getString("newnode.html"));
}
/** @return a new node with the given label, initialized with a style from the current editor property states */
public static LWNode createNewNode(String label) {
LWNode node = createDefaultNode(label);
EditorManager.targetAndApplyCurrentProperties(node);
return node;
}
// /** @return a new node with the given label initialized to internal VUE defaults -- ignore tool states */
// public static LWNode createNode(String label)
// {
// return createDefaultNode(label);
// }
/** @return a new node initialized to internal VUE defaults -- ignore tool states */
public static LWNode createDefaultNode(String label) {
return new LWNode(label);
}
/** @return a "text" node initialized to the current style in the VUE editors.
[old: Will adjust text size for current zoom level]
*/
public static LWNode createTextNode(String text)
{
LWNode node = buildTextNode(text);
EditorManager.targetAndApplyCurrentProperties(node);
// if (VUE.getActiveViewer() != null) {
// // Okay, for now this completely overrides the font size from the text toolbar...
// final Font font = node.getFont();
// final float curZoom = (float) VUE.getActiveViewer().getZoomFactor();
// final int minSize = LWNode.DEFAULT_TEXT_FONT.getSize();
// if (curZoom * font.getSize() < minSize)
// node.setFont(font.deriveFont(minSize / curZoom));
// }
return node;
}
/** @return a "text" node initialized to the current style in the VUE editors.
[old: Will adjust text size for current zoom level]
*/
public static LWText createRichTextNode(String text)
{
LWText node = buildRichTextNode(text);
node.getRichLabelBox(true).overrideTextColor(FontEditorPanel.mTextColorButton.getColor());
node.setAutoSized(false);
node.setSize(150,5);
return node;
}
private static LWNode initAsTextNode(LWNode node)
{
if (node != null)
node.setAsTextNode(true);
return node;
}
public static LWNode createDefaultTextNode(String text) {
LWNode node = new LWNode();
node.setLabel(text);
initAsTextNode(node);
return node;
}
// deprecate - use createDefaultTextNode
public static LWNode buildTextNode(String text) {
return createDefaultTextNode(text);
}
public static LWText buildRichTextNode(String s) {
LWText text = new LWText();
text.setLabel(s);
return text;
}
}
public Class<? extends Shape>[] getAllShapeClasses() {
Class<? extends Shape>[] classes = new Class[getSubToolIDs().size()];
int i = 0;
for (Object o : getAllShapeValues())
classes[i++] = ((Shape)o).getClass();
return classes;
}
public RectangularShape getNamedShape(String name)
{
if (mSubToolMap.isEmpty())
throw new Error("uninitialized sub-tools");
for (VueTool t : mSubToolMap.values()) {
//out("SUBTOOL " + t + " cssName=" + t.getAttribute("cssName"));
if (name.equalsIgnoreCase(t.getAttribute("cssName"))) {
return ((SubTool)t).getShape();
//final SubTool shapeTool = (SubTool) t;
//out("GOT SHAPE " + shapeTool.getShape());
//return shapeTool.getShape();
}
}
return null;
}
/**
* VueTool class for each of the specifc node shapes. Knows how to generate
* an action for shape setting, and creates a dynamic icon based on the node shape.
*/
public static class SubTool extends VueSimpleTool
{
private Class shapeClass = null;
private RectangularShape cachedShape = null;
private VueAction shapeSetterAction = null;
public SubTool() {}
public void setID(String pID) {
super.setID(pID);
//System.out.println(this + " ID set");
//getShape(); // cache it for fast response first time
setGeneratedIcons(new ShapeIcon(getShapeInstance()));
}
/** @return an action, with icon set, that will set the shape of selected
* LWNodes to the current shape for this SubTool */
public VueAction getShapeSetterAction() {
if (shapeSetterAction == null) {
shapeSetterAction = new Actions.LWCAction(getToolName(), new ShapeIcon(getShapeInstance())) {
void act(LWNode n) { n.setShapeInstance(getShapeInstance()); }
};
//shapeSetterAction.putValue("property.key", LWKey.Shape);
shapeSetterAction.putValue("property.value", getShape()); // this may be handy
// key is from: MenuButton.ValueKey
}
return shapeSetterAction;
}
public RectangularShape getShapeInstance()
{
if (shapeClass == null) {
String shapeClassName = getAttribute("shapeClass");
//System.out.println(this + " got shapeClass " + shapeClassName);
try {
this.shapeClass = getClass().getClassLoader().loadClass(shapeClassName);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
RectangularShape rectShape = null;
try {
rectShape = (RectangularShape) shapeClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return rectShape;
}
/** @return true if given shape is of same type as us */
public boolean isShape(RectangularShape shape) {
return shape != null && getShape().getClass().equals(shape.getClass());
}
public RectangularShape getShape()
{
if (cachedShape == null)
cachedShape = getShapeInstance();
return cachedShape;
}
static final int nearestEven(double d)
{
if (Math.floor(d) == d && d % 2 == 1) // if exact odd integer, just increment
return (int) d+1;
if (Math.floor(d) % 2 == 0)
return (int) Math.floor(d);
else
return (int) Math.ceil(d);
}
static final int nearestOdd(double d)
{
if (Math.floor(d) == d && d % 2 == 0) // if exact even integer, just increment
return (int) d+1;
if (Math.floor(d) % 2 == 1)
return (int) Math.floor(d);
else
return (int) Math.ceil(d);
}
//private static final Color sShapeColor = new Color(165,178,208); // Melanie's steel blue
private static final Color sShapeColor = new Color(93,98,162); // Melanie's icon blue/purple
private static final Color sShapeColorLight = VueUtil.factorColor(sShapeColor, 1.3);
//private static final boolean sPaintBorder = false;
private static GradientPaint sShapeGradient;
private static int sWidth;
private static int sHeight;
static {
// Select a width/height that will perfectly center within the parent button
// icon. If parent width is even, our width should be even, if odd, we
// should be odd. This is independent of the 50% size of the parent button
// we're using as a baseline (before the pixel tweak). This also means if
// somebody goes to center us in the parent (ToolIcon), that computation
// will always have an even integer result, thus perfectly pixel aligned.
// NOTE: if you paint a 1 pix border on the shape, when anti-aliased this
// generally adds a total of 1 pixel to the height & width. If painting a
// border on the dyanmic shape, you need to account for that for perfect
// centering.
if (ToolIcon.Width % 2 == 0)
sWidth = nearestOdd(ToolIcon.Width / 2);
else
sWidth = nearestEven(ToolIcon.Width / 2);
if (ToolIcon.Height % 2 == 0)
sHeight = nearestOdd(ToolIcon.Height / 2);
else
sHeight = nearestEven(ToolIcon.Height / 2);
sHeight--; // new priority: for even veritcal alignment in the combo-box -- SMF 2007-05-01
sShapeGradient = new GradientPaint(sWidth/2,0,sShapeColorLight, sWidth/2,sHeight/2,sShapeColor,true); // horizontal dark center
//sShapeGradient = new GradientPaint(sWidth/2,0,sShapeColor, sWidth/2,sHeight/2,sShapeColorLight,true); // horizontal light center
//sShapeGradient = new GradientPaint(0,sHeight/2,sShapeColor.brighter(), sWidth/2,sHeight/2,sShapeColor,true); // vertical
//sShapeGradient = new GradientPaint(0,0,sShapeColor.brighter(), sWidth/2,sHeight/2,sShapeColor,true); // diagonal
}
public static class ShapeIcon implements Icon
{
public int getIconWidth() { return sWidth; }
public int getIconHeight() { return sHeight; }
private RectangularShape mShape;
public ShapeIcon(RectangularShape pShape)
{
mShape = pShape;
if (mShape instanceof RoundRectangle2D) {
// hack to deal with arcs being too small on a tiny icon
((RoundRectangle2D)mShape).setRoundRect(0, 0, sWidth,sHeight, 8,8);
} else
mShape.setFrame(0,0, sWidth,sHeight);
}
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.translate(x,y);
// if (false) {
// if (sShapeGradient != null)
// g2.setPaint(sShapeGradient);
// else
// g2.setColor(sShapeColor);
// g2.fill(mShape);
// }
// final boolean enabled;
// if (c instanceof NodeToolPanel.ShapeMenuButton.ShapeComboRenderer) {
// // hack to extract true enabled state thru the renderer: better to make more
// enabled = ((NodeToolPanel.ShapeMenuButton.ShapeComboRenderer)c).getActionEnabled();
// } else {
// enabled = c.isEnabled();
// }
// if (enabled) {
// g2.setColor(Color.black);
// if (DEBUG.TOOL) System.out.println("DRAWING ENABLED: " + this + " on " + c);
// } else {
// g2.setColor(Color.lightGray);
// if (DEBUG.TOOL) System.out.println("DRAWING DISABLED: " + this + " on " + c);
// }
g2.setColor(Color.black);
g2.setStroke(STROKE_ONE);
g2.draw(mShape);
g2.translate(-x,-y);
}
/* @return true if representing the same *class* of shape.
*
* Currently shape's are differentiated
* by class only, which is why we have tufts.vue.shape.RoundRect2D with fixed arc widths as a separate
* class. (Additnal architecture will be needed if we want to get around this self-imposed limitation)
*
public boolean equals(Object o) {
return o == this ||
o != null && o instanceof ShapeIcon && ((ShapeIcon)o).mShape.getClass().equals(getClass());
// note: above will consider awt RoundRect2D's with different arcs as same shape... use tufts.vue.shape.*
// with fixed params to avoid this.
}
*/
public String toString() {
return "ShapeIcon[" + sWidth + "x" + sHeight + " " + mShape + "]";
}
}
}
public static class OntologyNodeTool extends VueTool implements edu.tufts.vue.ontology.ui.OntologySelectionListener
{
LWNode creationNode = null;
private static OntologyNodeTool singleton = null;
public OntologyNodeTool()
{
super();
edu.tufts.vue.ontology.ui.OntologyBrowser.getBrowser().addOntologySelectionListener(this);
setActiveWhileDownKeyCode(KeyEvent.VK_X);
VueToolUtils.setToolProperties(this,"ontologyNodeModeTool");
singleton=this;
}
public static OntologyNodeTool getTool()
{
if (singleton == null) {
// new Throwable("Warning: NodeTool.getTool: class not initialized by VUE").printStackTrace();
//throw new IllegalStateException("NodeTool.getTool: class not initialized by VUE");
new OntologyNodeTool();
}
return singleton;
}
@Override
public Class getSelectionType() { return LWNode.class; }
@Override
public boolean handleMousePressed(MapMouseEvent e) {
//out("MOUSE PRESSED");
// Get the creation node ready in case we're about to do a drag:
// (todo: VueTool.handleDragBegin)
// EditorManager.applyCurrentProperties(creationNode);
if (creationNode == null)
{
VueUtil.alert(VueResources.getString("ontologyNodeError.message"), VueResources.getString("ontologyNodeError.title"));
return true;
}
else
return false;
}
@Override
public boolean handleSelectorRelease(MapMouseEvent e)
{
//LWNode node = createNode(VueResources.getString("newnode.html"), true);
if (creationNode == null)
return false;
final LWNode node = (LWNode) creationNode.duplicate();
node.setAutoSized(false);
node.setFrame(e.getMapSelectorBox());
MapViewer viewer = e.getViewer();
viewer.getFocal().addChild(node);
VUE.getUndoManager().mark("New Node");
VUE.getSelection().setTo(node);
viewer.activateLabelEdit(node);
//creationNodeCurrent = false;
return true;
}
@Override
public void drawSelector(DrawContext dc, java.awt.Rectangle r)
{
dc.g.draw(r);
// TODO: doesn't handle zoom
// Also, handleSelectorRelease can now just dupe the creationNode
creationNode.setFrame(r);
creationNode.draw(dc);
}
edu.tufts.vue.ontology.ui.TypeList list = null;
public void ontologySelected(OntologySelectionEvent e) {
edu.tufts.vue.ontology.ui.TypeList l = e.getSelection();
if (list != l)
creationNode=null;
list = l;
LWComponent c = l.getSelectedComponent();
if (c != null)
{
if (c instanceof LWNode)
{
creationNode = (LWNode)c;
// EditorManager.targetAndApplyCurrentProperties(creationNode);
//EditorManager.targetAndApplyCurrentProperties(creationNode);
}
}
}
}
}