/*FreeMind - A Program for creating and viewing Mindmaps
*Copyright (C) 2000-2006 Joerg Mueller, Daniel Polansky, Christian Foltin, Dimitri Polivaev and others.
*See COPYING for Details
*
*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 2
*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, write to the Free Software
*Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package freemind.view.mindmapview;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.MalformedURLException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.SwingConstants;
import javax.swing.ToolTipManager;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeNode;
import freemind.controller.Controller;
import freemind.main.FreeMind;
import freemind.main.FreeMindMain;
import freemind.main.HtmlTools;
import freemind.main.Resources;
import freemind.main.Tools;
import freemind.modes.MindIcon;
import freemind.modes.MindMapCloud;
import freemind.modes.MindMapNode;
import freemind.modes.ModeController;
import freemind.modes.NodeAdapter;
import freemind.preferences.FreemindPropertyListener;
import freemind.view.mindmapview.attributeview.AttributeView;
/**
* This class represents a single Node of a MindMap (in analogy to
* TreeCellRenderer).
*/
public class NodeView extends JComponent implements TreeModelListener {
public void setFocusCycleRoot(boolean pFocusCycleRoot) {
// FIXME: On purpose removed. test this!
// super.setFocusCycleRoot(pFocusCycleRoot);
}
static private int FOLDING_SYMBOL_WIDTH = -1;
protected MindMapNode model;
protected MapView map;
private MainView mainView;
private AttributeView attributeView;
protected final static Color dragColor = Color.lightGray; // the Color of
// appearing
// GradientBox
// on
// drag over
private boolean left = true; // is the node left of root?
private boolean isLong = false;
public final static int DRAGGED_OVER_NO = 0;
public final static int DRAGGED_OVER_SON = 1;
public final static int DRAGGED_OVER_SIBLING = 2;
/** For RootNodeView. */
public final static int DRAGGED_OVER_SON_LEFT = 3;
final static int ALIGN_BOTTOM = -1;
final static int ALIGN_CENTER = 0;
final static int ALIGN_TOP = 1;
final private static Point zeroPoint = new Point(0, 0);
private static Logger logger;
private static FreemindPropertyListener sListener;
//
// Constructors
//
private Object viewDeletionEvent;
private int maxToolTipWidth;
private NodeView preferredChild;
private JComponent contentPane;
protected NodeMotionListenerView motionListenerView;
static final int SPACE_AROUND = 50;
private NodeFoldingComponent mFoldingListener;
protected NodeView(MindMapNode model, int position, MapView map,
Container parent) {
if (logger == null) {
logger = map.getController().getFrame()
.getLogger(this.getClass().getName());
}
if(sListener == null){
sListener = new FreemindPropertyListener() {
public void propertyChanged(String pPropertyName,
String pNewValue, String pOldValue) {
if (Tools.safeEquals(pPropertyName,
FreeMind.TOOLTIP_DISPLAY_TIME)) {
// control tooltip times:
ToolTipManager.sharedInstance().setDismissDelay(
Resources.getInstance().getIntProperty(
FreeMind.TOOLTIP_DISPLAY_TIME, 4000));
}
}
};
Controller.addPropertyChangeListenerAndPropagate(sListener);
}
setFocusCycleRoot(true);
this.model = model;
this.map = map;
final TreeNode parentNode = model.getParent();
final int index = parentNode == null ? 0 : parentNode.getIndex(model);
createAttributeView();
parent.add(this, index);
addFoldingListener();
}
protected void addFoldingListener() {
if(mFoldingListener == null && getModel().hasVisibleChilds() && !getModel().isRoot()) {
mFoldingListener = new NodeFoldingComponent(this);
add(mFoldingListener, getComponentCount()-1);
mFoldingListener.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent pE) {
getModeController().setFolded(getModel(), !getModel().isFolded());
}
});
}
}
protected void removeFoldingListener() {
if(mFoldingListener != null) {
mFoldingListener.dispose();
remove(mFoldingListener);
mFoldingListener = null;
}
}
public void propertyChanged(String pPropertyName, String pNewValue,
String pOldValue) {
}
void setMainView(MainView newMainView) {
if (mainView != null) {
final Container c = mainView.getParent();
int i;
for (i = c.getComponentCount() - 1; i >= 0
&& mainView != c.getComponent(i); i--) {
// left blank on purpose
}
c.remove(i);
ToolTipManager.sharedInstance().unregisterComponent(mainView);
mainView.removeMouseListener(this.map.getNodeMouseMotionListener());
mainView.removeMouseMotionListener(this.map
.getNodeMouseMotionListener());
c.add(newMainView, i);
} else {
add(newMainView);
}
this.mainView = newMainView;
ToolTipManager.sharedInstance().registerComponent(mainView);
mainView.addMouseListener(this.map.getNodeMouseMotionListener());
mainView.addMouseMotionListener(this.map.getNodeMouseMotionListener());
addDragListener(map.getNodeDragListener());
addDropListener(map.getNodeDropListener());
if (!model.isRoot() && "true".equals(map.getController().getProperty(FreeMindMain.ENABLE_NODE_MOVEMENT))) {
motionListenerView = new NodeMotionListenerView(this);
add(motionListenerView);
}
}
protected void removeFromMap() {
setFocusCycleRoot(false);
getParent().remove(this);
if (motionListenerView != null) {
remove(motionListenerView);
motionListenerView = null;
}
removeFoldingListener();
ToolTipManager.sharedInstance().unregisterComponent(mainView);
}
void addDragListener(DragGestureListener dgl) {
if (dgl == null)
return;
DragSource dragSource = DragSource.getDefaultDragSource();
dragSource.createDefaultDragGestureRecognizer(getMainView(),
DnDConstants.ACTION_COPY | DnDConstants.ACTION_MOVE
| DnDConstants.ACTION_LINK, dgl);
}
void addDropListener(DropTargetListener dtl) {
if (dtl == null)
return;
DropTarget dropTarget = new DropTarget(getMainView(), dtl);
dropTarget.setActive(true);
}
public boolean isRoot() {
return getModel().isRoot();
}
public boolean getIsLong() {
return isLong;
}
/* fc, 25.1.2004: Refactoring necessary: should call the model. */
public boolean isSiblingOf(NodeView myNodeView) {
return getParentView() == myNodeView.getParentView();
}
/* fc, 25.1.2004: Refactoring necessary: should call the model. */
public boolean isChildOf(NodeView myNodeView) {
return getParentView() == myNodeView;
}
/* fc, 25.1.2004: Refactoring necessary: should call the model. */
public boolean isParentOf(NodeView myNodeView) {
return (this == myNodeView.getParentView());
}
public MindMapNode getModel() {
return model;
}
/**
* Returns the coordinates occupied by the node and its children as a vector
* of four point per node.
*/
public void getCoordinates(LinkedList inList) {
getCoordinates(inList, 0, false, 0, 0);
}
private void getCoordinates(LinkedList inList,
int additionalDistanceForConvexHull, boolean byChildren,
int transX, int transY) {
if (!isVisible())
return;
if (isContentVisible()) {
MindMapCloud cloud = getModel().getCloud();
// consider existing clouds of children
if (byChildren && cloud != null) {
additionalDistanceForConvexHull += CloudView
.getAdditionalHeigth(cloud, this) / 5;
}
final int x = transX + getContent().getX() - getDeltaX();
final int y = transY + getContent().getY() - getDeltaY();
final int width = getMainViewWidthWithFoldingMark();
int heightWithFoldingMark = getMainViewHeightWithFoldingMark();
final int height = Math.max(heightWithFoldingMark, getContent()
.getHeight());
inList.addLast(new Point(-additionalDistanceForConvexHull + x,
-additionalDistanceForConvexHull + y));
inList.addLast(new Point(-additionalDistanceForConvexHull + x,
additionalDistanceForConvexHull + y + height));
inList.addLast(new Point(additionalDistanceForConvexHull + x
+ width, additionalDistanceForConvexHull + y + height));
inList.addLast(new Point(additionalDistanceForConvexHull + x
+ width, -additionalDistanceForConvexHull + y));
}
LinkedList childrenViews = getChildrenViews();
ListIterator children_it = childrenViews.listIterator();
while (children_it.hasNext()) {
NodeView child = (NodeView) children_it.next();
child.getCoordinates(inList, additionalDistanceForConvexHull, true,
transX + child.getX(), transY + child.getY());
}
}
/**
*/
public void setText(String string) {
mainView.setText(string);
}
/**
*/
public String getText() {
return mainView.getText();
}
protected int getMainViewWidthWithFoldingMark() {
return mainView.getMainViewWidthWithFoldingMark();
}
/** get height including folding symbol */
protected int getMainViewHeightWithFoldingMark() {
return mainView.getMainViewHeightWithFoldingMark();
}
/** get x coordinate including folding symbol */
public int getDeltaX() {
return mainView.getDeltaX();
}
/** get y coordinate including folding symbol */
public int getDeltaY() {
return mainView.getDeltaY();
}
public void requestFocus() {
// delegate to mapview:
getController().obtainFocusForSelected();
}
//
// get/set methods
//
/**
* Calculates the tree height increment because of the clouds.
*/
public int getAdditionalCloudHeigth() {
if (!isContentVisible()) {
return 0;
}
MindMapCloud cloud = getModel().getCloud();
if (cloud != null) {
return CloudView.getAdditionalHeigth(cloud, this);
} else {
return 0;
}
}
public boolean isSelected() {
return (getMap().isSelected(this));
}
/** Is the node left of root? */
public boolean isLeft() {
return getModel().isLeft();
}
protected void setModel(MindMapNode model) {
this.model = model;
}
public MapView getMap() {
return map;
}
protected Controller getController() {
return map.getController();
}
protected ModeController getModeController() {
return getMap().getModel().getModeController();
}
protected FreeMindMain getFrame() {
return getController().getFrame();
}
boolean isParentHidden() {
final Container parent = getParent();
if (!(parent instanceof NodeView))
return false;
NodeView parentView = (NodeView) parent;
return !parentView.isContentVisible();
}
public NodeView getParentView() {
final Container parent = getParent();
if (parent instanceof NodeView)
return (NodeView) parent;
return null;
}
public NodeView getVisibleParentView() {
final Container parent = getParent();
if (!(parent instanceof NodeView))
return null;
NodeView parentView = (NodeView) parent;
if (parentView.isContentVisible()) {
return parentView;
}
return parentView.getVisibleParentView();
}
/**
* This method returns the NodeViews that are children of this node.
*/
public LinkedList getChildrenViews() {
LinkedList childrenViews = new LinkedList();
final Component[] components = getComponents();
for (int i = 0; i < components.length; i++) {
if (!(components[i] instanceof NodeView)) {
continue;
}
NodeView view = (NodeView) components[i];
childrenViews.add(view); // child.getViewer() );
}
return childrenViews;
}
protected LinkedList getSiblingViews() {
return getParentView().getChildrenViews();
}
/**
* Returns the point the edge should start given the point of the child node
* that should be connected.
*
* @param targetView
* TODO
*/
Point getMainViewOutPoint(NodeView targetView, Point destinationPoint) {
final NodeViewLayout layoutManager = (NodeViewLayout) getLayout();
Point out = layoutManager.getMainViewOutPoint(this, targetView,
destinationPoint);
return out;
}
/**
* Returns the Point where the InEdge should arrive the Node.
*/
Point getMainViewInPoint() {
final NodeViewLayout layoutManager = (NodeViewLayout) getLayout();
Point in = layoutManager.getMainViewInPoint(this);
return in;
}
/**
* Returns the Point where the Links should arrive the Node.
*/
public Point getLinkPoint(Point declination) {
int x, y;
Point linkPoint;
if (declination != null) {
x = getMap().getZoomed(declination.x);
y = getMap().getZoomed(declination.y);
} else {
x = 1;
y = 0;
}
if (isLeft()) {
x = -x;
}
if (y != 0) {
double ctgRect = Math.abs((double) getContent().getWidth()
/ getContent().getHeight());
double ctgLine = Math.abs((double) x / y);
int absLinkX, absLinkY;
if (ctgRect > ctgLine) {
absLinkX = Math.abs(x * getContent().getHeight() / (2 * y));
absLinkY = getContent().getHeight() / 2;
} else {
absLinkX = getContent().getWidth() / 2;
absLinkY = Math.abs(y * getContent().getWidth() / (2 * x));
}
linkPoint = new Point(getContent().getWidth() / 2
+ (x > 0 ? absLinkX : -absLinkX), getContent().getHeight()
/ 2 + (y > 0 ? absLinkY : -absLinkY));
} else {
linkPoint = new Point((x > 0 ? getContent().getWidth() : 0),
(getContent().getHeight() / 2));
}
linkPoint.translate(getContent().getX(), getContent().getY());
convertPointToMap(linkPoint);
return linkPoint;
}
protected Point convertPointToMap(Point p) {
return Tools.convertPointToAncestor(this, p, getMap());
}
/**
* Returns the relative position of the Edge. This is used by bold edge to
* know how to shift the line.
*/
int getAlignment() {
return mainView.getAlignment();
}
//
// Navigation
//
/**
* The algorithm should be here the following (see Eclipse editor):
* Selected is the n-th node from above.
* Look for the last node visible on the screen and make this node the first one.
* Now select the n-th node from above.
*
* Easier idea to implement:
* Store node y position as y0.
* Search for a node with the same parent with y position y0+height
* Scroll the window by height.
*/
protected NodeView getNextPage() {
// from root we cannot jump
if (getModel().isRoot()) {
return this; // I'm root
}
int y0 = getInPointInMap().y + getMap().getViewportSize().height;
NodeView sibling = getNextVisibleSibling();
if (sibling == this) {
return this; // at the end
}
// if (sibling.getParentView() != this.getParentView()) {
// return sibling; // sibling on another page (has different parent)
// }
NodeView nextSibling = sibling.getNextVisibleSibling();
while (nextSibling != sibling
&& sibling.getParentView() == nextSibling.getParentView()) {
// has the same position after one page?
if(nextSibling.getInPointInMap().y >= y0) {
break;
}
sibling = nextSibling;
nextSibling = nextSibling.getNextVisibleSibling();
}
return sibling; // last on the page
}
/**
* @return the position of the in-point of this node in view coordinates
*/
protected Point getInPointInMap() {
return convertPointToMap(getMainViewInPoint());
}
protected NodeView getPreviousPage() {
if (getModel().isRoot()) {
return this; // I'm root
}
int y0 = getInPointInMap().y - getMap().getViewportSize().height;
NodeView sibling = getPreviousVisibleSibling();
if (sibling == this) {
return this; // at the end
}
// if (sibling.getParentView() != this.getParentView()) {
// return sibling; // sibling on another page (has different parent)
// }
NodeView previousSibling = sibling.getPreviousVisibleSibling();
while (previousSibling != sibling
&& sibling.getParentView() == previousSibling.getParentView()) {
// has the same position after one page?
if(previousSibling.getInPointInMap().y <= y0) {
break;
}
sibling = previousSibling;
previousSibling = previousSibling.getPreviousVisibleSibling();
}
return sibling; // last on the page
}
protected NodeView getNextVisibleSibling() {
NodeView sibling;
NodeView lastSibling = this;
// get next sibling even in higher levels
for (sibling = this; !sibling.getModel().isRoot(); sibling = sibling
.getParentView()) {
lastSibling = sibling;
sibling = sibling.getNextSiblingSingle();
if (sibling != lastSibling) {
break; // found sibling
}
}
// we have the nextSibling, search in childs
// untill: leaf, closed node, max level
while (sibling.getModel().getNodeLevel() < getMap()
.getSiblingMaxLevel()) {
// can we drill down?
NodeView first = sibling.getFirst(sibling.isRoot() ? lastSibling
: null, this.isLeft(), !this.isLeft());
if (first == null) {
break;
}
sibling = first;
}
if (sibling.isRoot()) {
return this; // didn't find (we are at the end)
}
return sibling;
}
/**
* @param startAfter
* TODO
*/
NodeView getFirst(Component startAfter, boolean leftOnly, boolean rightOnly) {
final Component[] components = getComponents();
for (int i = 0; i < components.length; i++) {
if (startAfter != null) {
if (components[i] == startAfter) {
startAfter = null;
}
continue;
}
if (!(components[i] instanceof NodeView)) {
continue;
}
NodeView view = (NodeView) components[i];
if (leftOnly && !view.isLeft() || rightOnly && view.isLeft()) {
continue;
}
if (view.isContentVisible()) {
return view;
}
NodeView child = view.getFirst(null, leftOnly, rightOnly);
if (child != null) {
return child;
}
}
return null;
}
/**
*/
public boolean isContentVisible() {
return getModel().isVisible();
}
private NodeView getLast(Component startBefore, boolean leftOnly,
boolean rightOnly) {
final Component[] components = getComponents();
for (int i = components.length - 1; i >= 0; i--) {
if (startBefore != null) {
if (components[i] == startBefore) {
startBefore = null;
}
continue;
}
if (!(components[i] instanceof NodeView)) {
continue;
}
NodeView view = (NodeView) components[i];
if (leftOnly && !view.isLeft() || rightOnly && view.isLeft()) {
continue;
}
if (view.isContentVisible()) {
return view;
}
NodeView child = view.getLast(null, leftOnly, rightOnly);
if (child != null) {
return child;
}
}
return null;
}
LinkedList getLeft(boolean onlyVisible) {
LinkedList all = getChildrenViews();
LinkedList left = new LinkedList();
for (ListIterator e = all.listIterator(); e.hasNext();) {
NodeView node = (NodeView) e.next();
if (node == null)
continue;
if (node.isLeft())
left.add(node);
}
return left;
}
LinkedList getRight(boolean onlyVisible) {
LinkedList all = getChildrenViews();
LinkedList right = new LinkedList();
for (ListIterator e = all.listIterator(); e.hasNext();) {
NodeView node = (NodeView) e.next();
if (node == null)
continue;
if (!node.isLeft())
right.add(node);
}
return right;
}
protected NodeView getPreviousVisibleSibling() {
NodeView sibling;
NodeView previousSibling = this;
// get Previous sibling even in higher levels
for (sibling = this; !sibling.getModel().isRoot(); sibling = sibling
.getParentView()) {
previousSibling = sibling;
sibling = sibling.getPreviousSiblingSingle();
if (sibling != previousSibling) {
break; // found sibling
}
}
// we have the PreviousSibling, search in childs
// untill: leaf, closed node, max level
while (sibling.getModel().getNodeLevel() < getMap()
.getSiblingMaxLevel()) {
NodeView last = sibling.getLast(sibling.isRoot() ? previousSibling
: null, this.isLeft(), !this.isLeft());
if (last == null) {
break;
}
sibling = last;
}
if (sibling.isRoot()) {
return this; // didn't find (we are at the end)
}
return sibling;
}
protected NodeView getNextSiblingSingle() {
LinkedList v = null;
if (getParentView().getModel().isRoot()) {
if (this.isLeft()) {
v = (getParentView()).getLeft(true);
} else {
v = (getParentView()).getRight(true);
}
} else {
v = getParentView().getChildrenViews();
}
final int index = v.indexOf(this);
for (int i = index + 1; i < v.size(); i++) {
final NodeView nextView = (NodeView) v.get(i);
if (nextView.isContentVisible()) {
return nextView;
} else {
final NodeView first = nextView.getFirst(null, false, false);
if (first != null) {
return first;
}
}
}
return this;
}
protected NodeView getPreviousSiblingSingle() {
LinkedList v = null;
if (getParentView().getModel().isRoot()) {
if (this.isLeft()) {
v = (getParentView()).getLeft(true);
} else {
v = (getParentView()).getRight(true);
}
} else {
v = getParentView().getChildrenViews();
}
final int index = v.indexOf(this);
for (int i = index - 1; i >= 0; i--) {
final NodeView nextView = (NodeView) v.get(i);
if (nextView.isContentVisible()) {
return nextView;
} else {
final NodeView last = nextView.getLast(null, false, false);
if (last != null) {
return last;
}
}
}
return this;
}
//
// Update from Model
//
void insert() {
ListIterator it = getModel().childrenFolded();
while (it.hasNext()) {
insert((MindMapNode) it.next(), 0);
}
}
/**
* Create views for the newNode and all his descendants, set their isLeft
* attribute according to this view.
*/
NodeView insert(MindMapNode newNode, int position) {
NodeView newView = NodeViewFactory.getInstance().newNodeView(newNode,
position, getMap(), this);
newView.insert();
return newView;
}
/**
* This is a bit problematic, because getChildrenViews() only works if model
* is not yet removed. (So do not _really_ delete the model before the view
* removed (it needs to stay in memory)
*/
void remove() {
for (ListIterator e = getChildrenViews().listIterator(); e.hasNext();) {
((NodeView) e.next()).remove();
}
if (isSelected()) {
getMap().deselect(this);
}
getModeController().onViewRemovedHook(this);
removeFromMap();
if (attributeView != null) {
attributeView.viewRemoved();
}
getModel().removeViewer(this); // Let the model know he is invisible
}
void update() {
updateStyle();
if (!isContentVisible()) {
// not visible at all
removeFoldingListener();
mainView.setVisible(false);
return;
}
mainView.setVisible(true);
updateTextColor();
updateFont();
updateIcons();
createAttributeView();
if (attributeView != null) {
attributeView.update();
}
// visible. has it still visible children?
if(getModel().hasVisibleChilds()) {
addFoldingListener();
} else {
removeFoldingListener();
}
updateText();
updateToolTip();
revalidate(); // Because of zoom?
}
public void createAttributeView() {
if (attributeView == null && model.getAttributes().getNode() != null) {
attributeView = new AttributeView(this);
}
}
void repaintSelected() {
updateTextColor();
repaint();
}
private void updateText() {
String nodeText = getModel().toString();
final boolean isHtml = nodeText.startsWith("<html>");
// 6) Set the text
// Right now, this implementation is quite logical, although it allows
// for nonconvex feature of nodes starting with <html>.
// For plain text, tell if node is long and its width has to be
// restricted
// boolean isMultiline = nodeText.indexOf("\n") >= 0;
boolean widthMustBeRestricted = false;
if (!isHtml) {
String[] lines = nodeText.split("\n");
for (int line = 0; line < lines.length; line++) {
// Compute the width the node would spontaneously take,
// by preliminarily setting the text.
setText(lines[line]);
widthMustBeRestricted = mainView.getPreferredSize().width > map
.getZoomed(map.getMaxNodeWidth())
+ mainView.getIconWidth();
if (widthMustBeRestricted) {
break;
}
}
isLong = widthMustBeRestricted || lines.length > 1;
}
if (isHtml) {
// Make it possible to use relative img references in HTML using tag
// <base>.
if (nodeText.indexOf("<img") >= 0 && nodeText.indexOf("<base ") < 0) {
try {
nodeText = "<html><base href=\"" + map.getModel().getURL()
+ "\">" + nodeText.substring(6);
} catch (MalformedURLException e) {
}
}
// If user does not want us to set the width automatically, he'll
// use <body width="">,
// <body width="800">, or avoid the <body> tag altogether.
// Set user HTML head
String htmlLongNodeHead = getFrame()
.getProperty("html_long_node_head");
if (htmlLongNodeHead != null && !htmlLongNodeHead.equals("")) {
if (nodeText.matches("(?ims).*<head>.*")) {
nodeText = nodeText.replaceFirst("(?ims).*<head>.*",
"<head>" + htmlLongNodeHead);
} else {
nodeText = nodeText.replaceFirst("(?ims)<html>",
"<html><head>" + htmlLongNodeHead + "</head>");
}
}
// Find out if the width has to be restricted.
if (nodeText.length() < 30000) {
// Empirically determined limit, above which we restrict the
// width without actually checking it.
// The purpose of that is to speed up rendering of very long
// nodes.
setText(nodeText);
widthMustBeRestricted = mainView.getPreferredSize().width > map
.getZoomed(map.getMaxNodeWidth())
+ mainView.getIconWidth();
} else {
widthMustBeRestricted = true;
}
if (widthMustBeRestricted) {
nodeText = nodeText.replaceFirst("(?i)<body>", "<body width=\""
+ map.getMaxNodeWidth() + "\">");
}
setText(nodeText);
} else if (nodeText.startsWith("<table>")) {
String[] lines = nodeText.split("\n");
lines[0] = lines[0].substring(7); // remove <table> tag
int startingLine = lines[0].matches("\\s*") ? 1 : 0;
// ^ If the remaining first line is empty, do not draw it
String text = "<html><table border=1 style=\"border-color: white\">";
// String[] lines = nodeText.split("\n");
for (int line = startingLine; line < lines.length; line++) {
text += "<tr><td style=\"border-color: white;\">"
+ HtmlTools.toXMLEscapedText(lines[line]).replaceAll(
"\t", "<td style=\"border-color: white\">");
}
setText(text);
} else if (isLong) {
String text = HtmlTools.plainToHTML(nodeText);
if (widthMustBeRestricted) {
text = text.replaceFirst("(?i)<p>",
"<p width=\"" + map.getMaxNodeWidth() + "\">");
}
setText(text);
} else {
setText(nodeText);
}
}
private void updateFont() {
Font font = getModel().getFont();
font = font == null ? getController().getDefaultFont() : font;
if (font != null) {
mainView.setFont(font);
} else {
// We can survive this trouble.
System.err.println("NodeView.update(): default font is null.");
}
}
private void updateIcons() {
updateIconPosition();
MultipleImage iconImages = new MultipleImage(1.0f);
boolean iconPresent = false;
/* fc, 06.10.2003: images? */
FreeMindMain frame = getFrame();
Map stateIcons = (getModel()).getStateIcons();
for (Iterator i = stateIcons.keySet().iterator(); i.hasNext();) {
String key = (String) i.next();
iconPresent = true;
ImageIcon myIcon = (ImageIcon) stateIcons.get(key);
iconImages.addImage(myIcon);
}
List icons = (getModel()).getIcons();
for (Iterator i = icons.iterator(); i.hasNext();) {
MindIcon myIcon = (MindIcon) i.next();
iconPresent = true;
// System.out.println("print the icon " + myicon.toString());
iconImages.addImage(myIcon.getIcon());
}
String link = ((NodeAdapter) getModel()).getLink();
if (link != null) {
iconPresent = true;
String iconPath = "images/Link.png";
if (link.startsWith("#")) {
iconPath = "images/LinkLocal.png";
} else if (link.startsWith("mailto:")) {
iconPath = "images/Mail.png";
} else if (Tools.executableByExtension(link)) {
iconPath = "images/Executable.png";
}
ImageIcon icon = new ImageIcon(frame.getResource(iconPath));
iconImages.addImage(icon);
}
// /* Folded icon by Matthias Schade (mascha2), fc, 20.12.2003*/
// if (((NodeAdapter)getModel()).isFolded()) {
// iconPresent = true;
// ImageIcon icon = new
// ImageIcon(((NodeAdapter)getModel()).getFrame().getResource("images/Folded.png"));
// iconImages.addImage(icon);
// }
// DanielPolansky: set icon only if icon is present, because
// we don't want to insert any additional white space.
setIcon(iconPresent ? iconImages : null);
}
private void updateIconPosition() {
getMainView().setHorizontalTextPosition(
isLeft() ? SwingConstants.LEADING : SwingConstants.TRAILING);
}
private void updateTextColor() {
Color color;
color = getModel().getColor();
if (color == null) {
color = MapView.standardNodeTextColor;
}
mainView.setForeground(color);
}
boolean useSelectionColors() {
return isSelected() && !MapView.standardDrawRectangleForSelection
&& !map.isCurrentlyPrinting();
}
void updateStyle() {
if (mainView != null
&& (mainView.getStyle().equals(model.getStyle()) || model
.isRoot())) {
return;
}
final MainView newMainView = NodeViewFactory.getInstance().newMainView(
model);
setMainView(newMainView);
}
/**
* Updates the tool tip of the node.
*/
public void updateToolTip() {
Map tooltips = getModel().getToolTip();
/*
* if(tooltips.size() == 1) { String toolTipText = (String)
* tooltips.values().iterator().next();
* logger.finest("setting tooltip to "+toolTipText);
* mainView.setToolTipText(toolTipText); } else
*/if (tooltips.size() == 0) {
mainView.setToolTipText(null);
} else {
// html table
StringBuffer text = new StringBuffer("<html><table width=\""
+ getMaxToolTipWidth() + "\">");
for (Iterator i = tooltips.keySet().iterator(); i.hasNext();) {
String key = (String) i.next();
String value = (String) tooltips.get(key);
// no html end inside the value:
value = value.replaceAll("</html>", "");
text.append("<tr><td>");
text.append(value);
text.append("</td></tr>");
}
text.append("</table></html>");
mainView.setToolTipText(text.toString());
}
}
public int getMaxToolTipWidth() {
if (maxToolTipWidth == 0) {
try {
maxToolTipWidth = getController().getIntProperty(
"max_tooltip_width", 600);
} catch (NumberFormatException e) {
maxToolTipWidth = 600;
}
}
return maxToolTipWidth;
}
/**
*/
public void setIcon(MultipleImage image) {
mainView.setIcon(image);
}
void updateAll() {
update();
invalidate();
for (ListIterator e = getChildrenViews().listIterator(); e.hasNext();) {
NodeView child = (NodeView) e.next();
child.updateAll();
}
}
String getStyle() {
return mainView.getStyle();
}
/**
* @return returns the color that should used to select the node.
*/
protected Color getSelectedColor() {
// Color backgroundColor = getModel().getBackgroundColor();
// // if(backgroundColor != null) {
// // Color backBrighter = backgroundColor.brighter();
// // // white?
// // if(backBrighter.getRGB() == Color.WHITE.getRGB()) {
// // return standardSelectColor;
// // }
// // // == standard??
// // if (backBrighter.equals (standardSelectColor) ) {
// // return backgroundColor.darker();
// // }
// // return backBrighter;
// // }
// // == standard??
// if (backgroundColor != null /*&&
// backgroundColor.equals(standardSelectColor)*/ ) {
// // bad hack:
// return getAntiColor1(backgroundColor);
// // return new Color(0xFFFFFF - backgroundColor.getRGB());
// }
return MapView.standardSelectColor;
}
/*
* http://groups.google.de/groups?hl=de&lr=&ie=UTF-8&threadm=9i5bbo%24h1kmi%243
* %
* 40ID-77081.news.dfncis.de&rnum=1&prev=/groups%3Fq%3Djava%2520komplement%25
* C3
* %25A4rfarbe%2520helligkeit%26hl%3Dde%26lr%3D%26ie%3DUTF-8%26sa%3DN%26as_qdr
* %3Dall%26tab%3Dwg
*/
/**
* Determines to a given color a color, that is the best contrary color. It
* is different from {@link #getAntiColor2}.
*
* @since PPS 1.1.1
*/
protected static Color getAntiColor1(Color c) {
float[] hsb = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(),
null);
hsb[0] += 0.40;
if (hsb[0] > 1)
hsb[0]--;
hsb[1] = 1;
hsb[2] = 0.7f;
return Color.getHSBColor(hsb[0], hsb[1], hsb[2]);
}
/**
* Determines to a given color a color, that is the best contrary color. It
* is different from {@link #getAntiColor1}.
*
* @since PPS 1.1.1
*/
protected static Color getAntiColor2(Color c) {
float[] hsb = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(),
null);
hsb[0] -= 0.40;
if (hsb[0] < 0)
hsb[0]++;
hsb[1] = 1;
hsb[2] = (float) 0.8;
return Color.getHSBColor(hsb[0], hsb[1], hsb[2]);
}
/**
* @return Returns the sHIFT.
*/
public int getShift() {
return map.getZoomed(model.calcShiftY());
}
/**
* @return Returns the VGAP.
*/
public int getVGap() {
return map.getZoomed(model.getVGap());
}
public int getHGap() {
return map.getZoomed(model.getHGap());
}
public MainView getMainView() {
return mainView;
}
void syncronizeAttributeView() {
if (attributeView != null) {
attributeView.syncronizeAttributeView();
}
}
public Font getTextFont() {
return getMainView().getFont();
}
public Color getTextColor() {
Color color = getModel().getColor();
if (color == null) {
color = MapView.standardNodeTextColor;
}
return color;
}
/**
*/
public AttributeView getAttributeView() {
if (attributeView == null) {
model.createAttributeTableModel();
attributeView = new AttributeView(this);
}
return attributeView;
}
public NodeView getPreferredVisibleChild(boolean left) { // mind preferred
// child
// :-) (PN)
if (preferredChild != null && (left == preferredChild.isLeft())
&& this.preferredChild.getParent() == this) {
if (preferredChild.isContentVisible()) {
return preferredChild;
} else {
NodeView newSelected = preferredChild
.getPreferredVisibleChild(left);
if (newSelected != null) {
return newSelected;
}
}
}
if (!getModel().isLeaf()) {
int yGap = Integer.MAX_VALUE;
final NodeView baseComponent;
if (isContentVisible()) {
baseComponent = this;
} else {
baseComponent = getVisibleParentView();
}
int ownY = baseComponent.getMainView().getY()
+ baseComponent.getMainView().getHeight() / 2;
NodeView newSelected = null;
for (int i = 0; i < getComponentCount(); i++) {
Component c = getComponent(i);
if (!(c instanceof NodeView)) {
continue;
}
NodeView childView = (NodeView) c;
if (!(childView.isLeft() == left)) {
continue;
}
if (!childView.isContentVisible()) {
childView = childView.getPreferredVisibleChild(left);
if (childView == null) {
continue;
}
}
Point childPoint = new Point(0, childView.getMainView()
.getHeight() / 2);
Tools.convertPointToAncestor(childView.getMainView(),
childPoint, baseComponent);
final int gapToChild = Math.abs(childPoint.y - ownY);
if (gapToChild < yGap) {
newSelected = childView;
preferredChild = (NodeView) c;
yGap = gapToChild;
} else {
break;
}
}
return newSelected;
}
return null;
}
public void setPreferredChild(NodeView view) {
this.preferredChild = view;
final Container parent = this.getParent();
if (view == null) {
return;
} else if (parent instanceof NodeView) {
// set also preffered child of parents...
((NodeView) parent).setPreferredChild(this);
}
}
/*
* (non-Javadoc)
*
* @see
* javax.swing.event.TreeModelListener#treeNodesChanged(javax.swing.event
* .TreeModelEvent)
*/
public void treeNodesChanged(TreeModelEvent e) {
update();
}
/*
* (non-Javadoc)
*
* @see
* javax.swing.event.TreeModelListener#treeNodesInserted(javax.swing.event
* .TreeModelEvent)
*/
public void treeNodesInserted(TreeModelEvent e) {
addFoldingListener();
if (getModel().isFolded()) {
return;
}
final int[] childIndices = e.getChildIndices();
for (int i = 0; i < childIndices.length; i++) {
int index = childIndices[i];
insert((MindMapNode) getModel().getChildAt(index), index);
}
revalidate();
}
/*
* (non-Javadoc)
*
* @see
* javax.swing.event.TreeModelListener#treeNodesRemoved(javax.swing.event
* .TreeModelEvent)
*/
public void treeNodesRemoved(TreeModelEvent e) {
if(!getModel().hasVisibleChilds()) {
removeFoldingListener();
}
getMap().resetShiftSelectionOrigin();
if (getModel().isFolded()) {
return;
}
final int[] childIndices = e.getChildIndices();
boolean preferredChildIsLeft = preferredChild != null
&& preferredChild.isLeft();
for (int i = childIndices.length - 1; i >= 0; i--) {
final int index = childIndices[i];
final NodeView node = (NodeView) getComponent(index);
if (node == this.preferredChild) { // mind preferred child :-) (PN)
this.preferredChild = null;
for (int j = index + 1; j < getComponentCount(); j++) {
final Component c = getComponent(j);
if (!(c instanceof NodeView)) {
break;
}
NodeView candidate = (NodeView) c;
if (candidate.isVisible()
&& node.isLeft() == candidate.isLeft()) {
this.preferredChild = candidate;
break;
}
}
if (this.preferredChild == null) {
for (int j = index - 1; j >= 0; j--) {
final Component c = getComponent(j);
if (!(c instanceof NodeView)) {
break;
}
NodeView candidate = (NodeView) c;
if (candidate.isVisible()
&& node.isLeft() == candidate.isLeft()) {
this.preferredChild = candidate;
break;
}
}
}
}
node.remove();
}
NodeView preferred = getPreferredVisibleChild(preferredChildIsLeft);
if (preferred != null) { // after delete focus on a brother (PN)
getMap().selectAsTheOnlyOneSelected(preferred);
} else {
getMap().selectAsTheOnlyOneSelected(this);
}
revalidate();
}
/*
* (non-Javadoc)
*
* @see
* javax.swing.event.TreeModelListener#treeStructureChanged(javax.swing.
* event.TreeModelEvent)
*/
public void treeStructureChanged(TreeModelEvent e) {
getMap().resetShiftSelectionOrigin();
for (ListIterator i = getChildrenViews().listIterator(); i.hasNext();) {
((NodeView) i.next()).remove();
}
insert();
if (map.getSelected() == null) {
map.selectAsTheOnlyOneSelected(this);
}
map.revalidateSelecteds();
revalidate();
}
public int getZoomedFoldingSymbolHalfWidth() {
int preferredFoldingSymbolHalfWidth = (int) ((getFoldingSymbolWidth() * map
.getZoom()) / 2);
return Math.min(preferredFoldingSymbolHalfWidth, getHeight() / 2);
}
/**
* @return the left/right point of the folding circle. To receive its
* center, the amount has to be moved to left/right (depending on its side)
* by the folding circle width.
*/
public Point getFoldingMarkPosition() {
Point out = getMainViewOutPoint(this, new Point());
return out;
}
public JComponent getContent() {
return contentPane == null ? mainView : contentPane;
}
public Container getContentPane() {
if (contentPane == null) {
contentPane = NodeViewFactory.getInstance().newContentPane(this);
remove(mainView);
contentPane.add(mainView);
add(contentPane);
}
return contentPane;
}
public NodeMotionListenerView getMotionListenerView() {
return motionListenerView;
}
public void setBounds(int x, int y, int width, int height) {
super.setBounds(x, y, width, height);
if (motionListenerView != null) {
motionListenerView.invalidate();
}
if (mFoldingListener != null) {
mFoldingListener.invalidate();
}
}
public void setVisible(boolean isVisible) {
super.setVisible(isVisible);
if (motionListenerView != null) {
motionListenerView.setVisible(isVisible);
}
if (mFoldingListener != null) {
mFoldingListener.setVisible(isVisible);
}
}
private void paintCloudsAndEdges(Graphics2D g) {
Object renderingHint = getController().setEdgesRenderingHint(g);
for (int i = 0; i < getComponentCount(); i++) {
final Component component = getComponent(i);
if (!(component instanceof NodeView)) {
continue;
}
NodeView nodeView = (NodeView) component;
if (nodeView.isContentVisible()) {
Point p = new Point();
Tools.convertPointToAncestor(nodeView, p, this);
g.translate(p.x, p.y);
nodeView.paintCloud(g);
g.translate(-p.x, -p.y);
EdgeView edge = NodeViewFactory.getInstance().getEdge(nodeView);
edge.paint(nodeView, g);
} else {
nodeView.paintCloudsAndEdges(g);
}
}
Tools.restoreAntialiasing(g, renderingHint);
}
/*
* (non-Javadoc)
*
* @see javax.swing.JComponent#paint(java.awt.Graphics)
*/
public void paint(Graphics g) {
final boolean isRoot = isRoot();
if (isRoot) {
paintCloud(g);
}
if (isContentVisible()) {
Graphics2D g2d = (Graphics2D) g;
paintCloudsAndEdges(g2d);
super.paint(g);
// return to std stroke
g2d.setStroke(BubbleMainView.DEF_STROKE);
// if (!isRoot) {
// paintFoldingMark(g2d);
// }
} else {
super.paint(g);
}
// g.setColor(Color.BLACK);
// g.drawRect(0, 0, getWidth()-1, getHeight()-1);
}
private void paintCloud(Graphics g) {
if (isContentVisible() && model.getCloud() != null) {
CloudView cloud = new CloudView(model.getCloud(), this);
cloud.paint(g);
}
}
/*
* (non-Javadoc)
*
* @see java.awt.Component#toString()
*/
public String toString() {
return getModel().toString() + ", " + super.toString();
}
public Rectangle getInnerBounds() {
final int space = getMap().getZoomed(SPACE_AROUND);
return new Rectangle(space, space, getWidth() - 2 * space, getHeight()
- 2 * space);
}
public boolean contains(int x, int y) {
final int space = getMap().getZoomed(SPACE_AROUND) - 2
* getZoomedFoldingSymbolHalfWidth();
return (x >= space) && (x < getWidth() - space) && (y >= space)
&& (y < getHeight() - space);
}
public Color getTextBackground() {
final Color modelBackgroundColor = getModel().getBackgroundColor();
if (modelBackgroundColor != null) {
return modelBackgroundColor;
}
return getBackgroundColor();
}
private Color getBackgroundColor() {
final MindMapCloud cloud = getModel().getCloud();
if (cloud != null) {
return cloud.getColor();
}
if (isRoot()) {
return getMap().getBackground();
}
return getParentView().getBackgroundColor();
}
public static int getFoldingSymbolWidth() {
if (FOLDING_SYMBOL_WIDTH == -1) {
FOLDING_SYMBOL_WIDTH = Resources.getInstance().getIntProperty(
"foldingsymbolwidth", 8);
}
return FOLDING_SYMBOL_WIDTH;
}
}