/*FreeMind - A Program for creating and viewing Mindmaps
*Copyright (C) 2000-2001 Joerg Mueller <joergmueller@bigfoot.com>
*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.modes;
import java.awt.Color;
import java.awt.Font;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Vector;
import javax.swing.ImageIcon;
import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import freemind.controller.Controller;
import freemind.controller.filter.Filter;
import freemind.controller.filter.FilterInfo;
import freemind.extensions.DontSaveMarker;
import freemind.extensions.NodeHook;
import freemind.extensions.PermanentNodeHook;
import freemind.main.FreeMind;
import freemind.main.FreeMindCommon;
import freemind.main.FreeMindMain;
import freemind.main.HtmlTools;
import freemind.main.Resources;
import freemind.main.Tools;
import freemind.main.XMLElement;
import freemind.modes.attributes.Attribute;
import freemind.modes.attributes.NodeAttributeTableModel;
import freemind.preferences.FreemindPropertyListener;
import freemind.view.mindmapview.NodeView;
import freemind.view.mindmapview.NodeViewVisitor;
/**
* This class represents a single Node of a Tree. It contains direct handles to
* its parent and children and to its view.
*/
public abstract class NodeAdapter implements MindMapNode {
final static int SHIFT = -2;// height of the vertical shift between node and
// its closest child
public final static int HGAP = 20;// width of the horizontal gap that
// contains the edges
public final static int VGAP = 3;// height of the vertical gap between nodes
public final static int LEFT_POSITION = -1;
public final static int RIGHT_POSITION = 1;
public final static int UNKNOWN_POSITION = 0;
private HashSet activatedHooks;
private List hooks;
protected Object userObject = "no text";
private String xmlText = "no text";
private String link = null; // Change this to vector in future for full
// graph support
private TreeMap toolTip = null; // lazy, fc, 30.6.2005
// these Attributes have default values, so it can be useful to directly
// access them in
// the save() method instead of using getXXX(). This way the stored file is
// smaller and looks better.
// (if the default is used, it is not stored) Look at mindmapmode for an
// example.
protected String style;
/** stores the icons associated with this node. */
protected Vector/* <MindIcon> */icons = null; // lazy, fc, 30.6.2005
protected TreeMap /* of String to MindIcon s */stateIcons = null; // lazy, fc,
// 30.6.2005
// /**stores the label associated with this node:*/
// protected String mLabel;
/** parameters of an eventually associated cloud */
protected MindMapCloud cloud;
protected Color color;
protected Color backgroundColor;
protected boolean folded;
private int position = UNKNOWN_POSITION;
private int vGap = VGAP;
private int hGap = HGAP;
private int shiftY = 0;
protected List children;
private MindMapNode preferredChild;
protected Font font;
protected boolean underlined = false;
private FilterInfo filterInfo = new FilterInfo();
private MindMapNode parent;
/**
* the edge which leads to this node, only root has none In future it has to
* hold more than one view, maybe with a Vector in which the index specifies
* the MapView which contains the NodeViews
*/
private MindMapEdge edge;
private Collection views = null;
private FreeMindMain frame;
private static final boolean ALLOWSCHILDREN = true;
private static final boolean ISLEAF = false; // all nodes may have children
/** read only empty attribute class */
private static final NodeAttributeTableModel EMTPY_ATTRIBUTES = new NodeAttributeTableModel(
null) {
public void insertRow(int index, String name, String value) {
throw new IllegalArgumentException(
"Can't set attributes in the EMTPY_ATTRIBUTES table.");
};
public void addRowNoUndo(Attribute newAttribute) {
throw new IllegalArgumentException(
"Can't set attributes in the EMTPY_ATTRIBUTES table.");
};
public Vector getAttributes() {
return new Vector();
};
};
private HistoryInformation historyInformation = null;
// Logging:
static protected java.util.logging.Logger logger;
private MindMap map = null;
private NodeAttributeTableModel attributes;
private String noteText;
private String xmlNoteText;
private static FreemindPropertyListener sSaveIdPropertyChangeListener;
private static boolean sSaveOnlyIntrinsicallyNeededIds = false;
//
// Constructors
//
protected NodeAdapter(FreeMindMain frame, MindMap map) {
this(null, frame, map);
}
protected NodeAdapter(Object userObject, FreeMindMain frame, MindMap map) {
this.frame = frame;
setText((String) userObject);
hooks = null; // lazy, fc, 30.6.2005.
activatedHooks = null; // lazy, fc, 30.6.2005
if (logger == null)
logger = frame.getLogger(this.getClass().getName());
// create creation time:
setHistoryInformation(new HistoryInformation());
this.map = map;
this.attributes = EMTPY_ATTRIBUTES;
if (sSaveIdPropertyChangeListener == null) {
sSaveIdPropertyChangeListener = new FreemindPropertyListener() {
public void propertyChanged(String propertyName,
String newValue, String oldValue) {
if (propertyName
.equals(FreeMindCommon.SAVE_ONLY_INTRISICALLY_NEEDED_IDS)) {
sSaveOnlyIntrinsicallyNeededIds = Boolean.valueOf(
newValue).booleanValue();
}
}
};
Controller
.addPropertyChangeListenerAndPropagate(sSaveIdPropertyChangeListener);
}
}
/**
*/
public void setMap(MindMap map) {
this.map = map;
map.getRegistry().registrySubtree(this, true);
}
public String getText() {
String string = "";
if (userObject != null) {
string = userObject.toString();
}
return string;
}
public final void setText(String text) {
if (text == null) {
userObject = null;
xmlText = null;
return;
}
userObject = HtmlTools.makeValidXml(text);
xmlText = HtmlTools.getInstance().toXhtml((String) userObject);
}
public final String getXmlText() {
return xmlText;
}
public final void setXmlText(String pXmlText) {
this.xmlText = HtmlTools.makeValidXml(pXmlText);
userObject = HtmlTools.getInstance().toHtml(xmlText);
}
/* ************************************************************
* ******** Notes *******
* ************************************************************
*/
public final String getXmlNoteText() {
return xmlNoteText;
}
public final String getNoteText() {
// logger.info("Note html: " + noteText);
return noteText;
}
public final void setXmlNoteText(String pXmlNoteText) {
if (pXmlNoteText == null) {
xmlNoteText = null;
noteText = null;
return;
}
this.xmlNoteText = HtmlTools.makeValidXml(pXmlNoteText);
noteText = HtmlTools.getInstance().toHtml(xmlNoteText);
}
public final void setNoteText(String pNoteText) {
if (pNoteText == null) {
xmlNoteText = null;
noteText = null;
return;
}
this.noteText = HtmlTools.makeValidXml(pNoteText);
this.xmlNoteText = HtmlTools.getInstance().toXhtml(noteText);
}
public String getPlainTextContent() {
// Redefined in MindMapNodeModel.
return toString();
}
public String getLink() {
return link;
}
public String getShortText(ModeController controller) {
String adaptedText = getPlainTextContent();
// adaptedText = adaptedText.replaceAll("<html>", "");
if (adaptedText.length() > 40)
adaptedText = adaptedText.substring(0, 40) + " ...";
return adaptedText;
}
public void setLink(String link) {
if (link != null && link.startsWith("#")) {
getMap().getLinkRegistry().registerLocalHyperlinkId(
link.substring(1));
}
this.link = link;
}
public FilterInfo getFilterInfo() {
return filterInfo;
}
public FreeMindMain getFrame() {
return frame;
}
//
// Interface MindMapNode
//
//
// get/set methods
//
public Collection getViewers() {
if (views == null) {
views = new LinkedList();
}
return views;
}
public void addViewer(NodeView viewer) {
getViewers().add(viewer);
addTreeModelListener(viewer);
}
public void removeViewer(NodeView viewer) {
getViewers().remove(viewer);
removeTreeModelListener(viewer);
}
/** Creates the TreePath recursively */
public TreePath getPath() {
Vector pathVector = new Vector();
TreePath treePath;
this.addToPathVector(pathVector);
treePath = new TreePath(pathVector.toArray());
return treePath;
}
public MindMapEdge getEdge() {
return edge;
}
public void setEdge(MindMapEdge edge) {
this.edge = edge;
}
public MindMapCloud getCloud() {
return cloud;
}
public void setCloud(MindMapCloud cloud) {
// Take care to keep the calculated iterative levels consistent
if (cloud != null && this.cloud == null) {
changeChildCloudIterativeLevels(1);
} else if (cloud == null && this.cloud != null) {
changeChildCloudIterativeLevels(-1);
}
this.cloud = cloud;
}
/**
* Correct iterative level values of children
*/
private void changeChildCloudIterativeLevels(int deltaLevel) {
for (ListIterator e = childrenUnfolded(); e.hasNext();) {
NodeAdapter childNode = (NodeAdapter) e.next();
MindMapCloud childCloud = childNode.getCloud();
if (childCloud != null) {
childCloud.changeIterativeLevel(deltaLevel);
}
childNode.changeChildCloudIterativeLevels(deltaLevel);
}
}
/** A Node-Style like MindMapNode.STYLE_FORK or MindMapNode.STYLE_BUBBLE */
public String getStyle() {
String returnedString = style; /* Style string returned */
if (style == null) {
if (this.isRoot()) {
returnedString = getFrame().getProperty(
FreeMind.RESOURCES_ROOT_NODE_STYLE);
} else {
String stdstyle = getFrame().getProperty(
FreeMind.RESOURCES_NODE_STYLE);
if (stdstyle.equals(MindMapNode.STYLE_AS_PARENT)) {
returnedString = getParentNode().getStyle();
} else {
returnedString = stdstyle;
}
}
} else if (this.isRoot() && style.equals(MindMapNode.STYLE_AS_PARENT)) {
returnedString = getFrame().getProperty(
FreeMind.RESOURCES_ROOT_NODE_STYLE);
} else if (style.equals(MindMapNode.STYLE_AS_PARENT)) {
returnedString = getParentNode().getStyle();
}
// Handle the combined node style
if (returnedString.equals(MindMapNode.STYLE_COMBINED)) {
if (this.isFolded()) {
return MindMapNode.STYLE_BUBBLE;
} else {
return MindMapNode.STYLE_FORK;
}
}
return returnedString;
}
public boolean hasStyle() {
return style != null;
}
/** The Foreground/Font Color */
public Color getColor() {
return color;
}
// ////
// The set methods. I'm not sure if they should be here or in the
// implementing class.
// ///
public void setStyle(String style) {
this.style = style;
}
public void setColor(Color color) {
this.color = color;
}
// fc, 24.2.2004: background color:
public Color getBackgroundColor() {
return backgroundColor;
};
public void setBackgroundColor(Color color) {
this.backgroundColor = color;
};
//
// font handling
//
// Remark to setBold and setItalic implemetation
//
// Using deriveFont() is a bad idea, because it does not really choose
// the appropriate face. For example, instead of choosing face
// "Arial Bold", it derives the bold face from "Arial".
// Node holds font only in the case that the font is not default.
public void establishOwnFont() {
font = (font != null) ? font : getFrame().getController()
.getDefaultFont();
}
public void setBold(boolean bold) {
if (bold != isBold()) {
toggleBold();
}
}
public void toggleBold() {
establishOwnFont();
setFont(getFrame().getController().getFontThroughMap(
new Font(font.getFamily(), font.getStyle() ^ Font.BOLD, font
.getSize())));
}
public void setItalic(boolean italic) {
if (italic != isItalic()) {
toggleItalic();
}
}
public void toggleItalic() {
establishOwnFont();
setFont(getFrame().getController().getFontThroughMap(
new Font(font.getFamily(), font.getStyle() ^ Font.ITALIC, font
.getSize())));
}
public void setUnderlined(boolean underlined) {
this.underlined = underlined;
}
public void setFont(Font font) {
this.font = font;
}
public MindMapNode getParentNode() {
return parent;
}
public void setFontSize(int fontSize) {
establishOwnFont();
setFont(getFrame().getController().getFontThroughMap(
new Font(font.getFamily(), font.getStyle(), fontSize)));
}
public Font getFont() {
return font;
}
public String getFontSize() {
if (getFont() != null) {
return new Integer(getFont().getSize()).toString();
} else {
return getFrame().getProperty("defaultfontsize");
}
}
public String getFontFamilyName() {
if (getFont() != null) {
return getFont().getFamily();
} else {
return getFrame().getProperty("defaultfont");
}
}
public boolean isBold() {
return font != null ? font.isBold() : false;
}
public boolean isItalic() {
return font != null ? font.isItalic() : false;
}
public boolean isUnderlined() { // not implemented
return underlined;
}
public boolean isFolded() {
return folded;
}
// fc, 24.9.2003:
public List getIcons() {
if (icons == null)
return Collections.EMPTY_LIST;
return icons;
}
public MindMap getMap() {
return map;
}
public void addIcon(MindIcon _icon, int position) {
createIcons();
if (position == MindIcon.LAST) {
icons.add(_icon);
} else {
icons.add(position, _icon);
}
getMap().getRegistry().addIcon(_icon);
}
/** @return returns the number of remaining icons. */
public int removeIcon(int position) {
createIcons();
if (position == MindIcon.LAST) {
position = icons.size() - 1;
}
icons.remove(position);
int returnSize = icons.size();
if (returnSize == 0) {
icons = null;
}
return returnSize;
};
// end, fc, 24.9.2003
// public String getLabel() { return mLabel; }
// public void setLabel(String newLabel) { mLabel = newLabel; /* bad hack:
// registry fragen.*/ };
// public Vector/* of NodeLinkStruct*/ getReferences() { return
// mNodeLinkVector; };
// public void removeReferenceAt(int i) {
// if(mNodeLinkVector.size() > i) {
// mNodeLinkVector.removeElementAt(i);
// } else {
// /* exception. */
// }
// }
// public void addReference(MindMapLink mindMapLink) {
// mNodeLinkVector.add(mindMapLink); };
/**
* True iff one of node's <i>strict</i> descendants is folded. A node N is
* not its strict descendant - the fact that node itself is folded is not
* sufficient to return true.
*/
public boolean hasFoldedStrictDescendant() {
for (ListIterator e = childrenUnfolded(); e.hasNext();) {
NodeAdapter child = (NodeAdapter) e.next();
if (child.isFolded() || child.hasFoldedStrictDescendant()) {
return true;
}
}
return false;
}
/**
* @return true, if one of its parents is folded. If itself is folded,
* doesn't matter.
*/
public boolean hasFoldedParents() {
if (isRoot())
return false;
if (getParentNode().isFolded()) {
return true;
}
return getParentNode().hasFoldedParents();
}
public void setFolded(boolean folded) {
this.folded = folded;
}
public MindMapNode shallowCopy() {
try {
// get XML from me.
StringWriter writer = new StringWriter();
this.save(writer, this.getMap().getLinkRegistry(), true, false);
String result = writer.toString();
HashMap IDToTarget = new HashMap();
MindMapNode copy = this.getModeController().createNodeTreeFromXml(
new StringReader(result), IDToTarget);
copy.setFolded(false);
return copy;
} catch (Exception e) {
freemind.main.Resources.getInstance().logException(e);
return null;
}
}
//
// other
//
public String toString() {
return getText();
}
public boolean isDescendantOf(MindMapNode pParentNode) {
if (this.isRoot())
return false;
else if (pParentNode == getParentNode())
return true;
else
return getParentNode().isDescendantOf(pParentNode);
}
public boolean isRoot() {
return (parent == null);
}
public boolean isDescendantOfOrEqual(MindMapNode pParentNode) {
if (this == pParentNode) {
return true;
}
return isDescendantOf(pParentNode);
}
public boolean hasChildren() {
return children != null && !children.isEmpty();
}
public int getChildPosition(MindMapNode childNode) {
int position = 0;
for (ListIterator i = children.listIterator(); i.hasNext(); ++position) {
if (((MindMapNode) i.next()) == childNode) {
return position;
}
}
return -1;
}
public ListIterator childrenUnfolded() {
return children != null ? children.listIterator()
: Collections.EMPTY_LIST.listIterator();
}
/*
* (non-Javadoc)
*
* @see freemind.modes.MindMapNode#sortedChildrenUnfolded()
*/
public ListIterator sortedChildrenUnfolded() {
if (children == null)
return null;
LinkedList sorted = new LinkedList(children);
/*
* Using this stable sort, we assure that the left nodes came in front
* of the right ones.
*/
Collections.sort(sorted, new Comparator() {
public int compare(Object pO1, Object pO2) {
return comp(((MindMapNode) pO2).isLeft(),
((MindMapNode) pO1).isLeft());
}
private int comp(boolean pLeft, boolean pLeft2) {
if (pLeft == pLeft2) {
return 0;
}
if (pLeft) {
return 1;
}
return -1;
}
});
return sorted.listIterator();
}
public ListIterator childrenFolded() {
if (isFolded()) {
return Collections.EMPTY_LIST.listIterator();
}
return childrenUnfolded();
}
public List getChildren() {
return Collections.unmodifiableList((children != null) ? children
: Collections.EMPTY_LIST);
}
//
// Interface TreeNode
//
/**
* AFAIK there is no way to get an enumeration out of a linked list. So this
* exception must be thrown, or we can't implement TreeNode anymore (maybe
* we shouldn't?)
*/
public Enumeration children() {
throw new UnsupportedOperationException(
"Use childrenFolded or childrenUnfolded instead");
}
public boolean getAllowsChildren() {
return ALLOWSCHILDREN;
}
public TreeNode getChildAt(int childIndex) {
// fc, 11.12.2004: This is not understandable, that a child does not
// exist if the parent is folded.
// if (isFolded()) {
// return null;
// }
return (TreeNode) children.get(childIndex);
}
public int getChildCount() {
return children == null ? 0 : children.size();
}
// (PN)
// public int getChildCount() {
// if (isFolded()) {
// return 0;
// }
// return children.size();
// }
// // Daniel: ^ The name of this method is confusing. It does nto convey
// // the meaning, at least not to me.
public int getIndex(TreeNode node) {
return children.indexOf((MindMapNode) node); // uses equals()
}
public TreeNode getParent() {
return parent;
}
public boolean isLeaf() {
return getChildCount() == 0;
}
// fc, 16.12.2003 left-right bug:
public boolean isLeft() {
if (getParent() != null && !getParentNode().isRoot()) {
return getParentNode().isLeft();
}
if (position == UNKNOWN_POSITION && !isRoot()) {
setLeft(getParentNode().isLeft());
}
return position == LEFT_POSITION;
}
public void setLeft(boolean isLeft) {
position = isLeft ? LEFT_POSITION : RIGHT_POSITION;
if (!isRoot()) {
for (int i = 0; i < getChildCount(); i++) {
final NodeAdapter child = (NodeAdapter) getChildAt(i);
child.position = position;
}
}
}
public boolean isNewChildLeft() {
if (!isRoot()) {
return isLeft();
}
int rightChildrenCount = 0;
for (int i = 0; i < getChildCount(); i++) {
if (!((MindMapNode) getChildAt(i)).isLeft())
rightChildrenCount++;
if (rightChildrenCount > getChildCount() / 2) {
return true;
}
}
return false;
}
//
// Interface MutableTreeNode
//
// do all remove methods have to work recursively to make the
// Garbage Collection work (Nodes in removed Sub-Trees reference each
// other)?
public void insert(MutableTreeNode child, int index) {
logger.finest("Insert at " + index + " the node " + child);
final MindMapNode childNode = (MindMapNode) child;
if (index < 0) { // add to the end (used in xml load) (PN)
index = getChildCount();
children.add(index, child);
} else { // mind preferred child :-)
children.add(index, child);
preferredChild = childNode;
}
child.setParent(this);
recursiveCallAddChildren(this, childNode);
}
public void remove(int index) {
MutableTreeNode node = (MutableTreeNode) children.get(index);
remove(node);
}
public void remove(MutableTreeNode node) {
if (node == this.preferredChild) { // mind preferred child :-) (PN)
int index = children.indexOf(node);
if (children.size() > index + 1) {
this.preferredChild = (MindMapNode) (children.get(index + 1));
} else {
this.preferredChild = (index > 0) ? (MindMapNode) (children
.get(index - 1)) : null;
}
}
node.setParent(null);
children.remove(node);
// call remove child hook after removal.
recursiveCallRemoveChildren(this, (MindMapNode) node, this);
}
private void recursiveCallAddChildren(MindMapNode node,
MindMapNode addedChild) {
// Tell any node hooks that the node is added:
if (node instanceof MindMapNode) {
for (Iterator i = ((MindMapNode) node).getActivatedHooks()
.iterator(); i.hasNext();) {
PermanentNodeHook hook = (PermanentNodeHook) i.next();
if (addedChild.getParentNode() == node) {
hook.onAddChild(addedChild);
}
hook.onAddChildren(addedChild);
}
}
if (!node.isRoot() && node.getParentNode() != null)
recursiveCallAddChildren(node.getParentNode(), addedChild);
}
/**
* @param oldDad
* the last dad node had.
*/
private void recursiveCallRemoveChildren(MindMapNode node,
MindMapNode removedChild, MindMapNode oldDad) {
for (Iterator i = node.getActivatedHooks().iterator(); i.hasNext();) {
PermanentNodeHook hook = (PermanentNodeHook) i.next();
if (removedChild.getParentNode() == node) {
hook.onRemoveChild(removedChild);
}
hook.onRemoveChildren(removedChild, oldDad);
}
if (!node.isRoot() && node.getParentNode() != null)
recursiveCallRemoveChildren(node.getParentNode(), removedChild,
oldDad);
}
public void removeFromParent() {
parent.remove(this);
}
public void setParent(MutableTreeNode newParent) {
parent = (MindMapNode) newParent;
}
public void setParent(MindMapNode newParent) {
parent = newParent;
}
public void setUserObject(Object object) {
setText((String) object);
}
// //////////////
// Private methods. Internal Implementation
// ////////////
/** Recursive Method for getPath() */
private void addToPathVector(Vector pathVector) {
pathVector.add(0, this); // Add myself to beginning of Vector
if (parent != null) {
((NodeAdapter) parent).addToPathVector(pathVector);
}
}
public int getNodeLevel() { // for cursor navigation within a level (PN)
int level = 0;
MindMapNode parent;
for (parent = this; !parent.isRoot(); parent = parent.getParentNode()) {
if (parent.isVisible()) {
level++;
}
}
return level;
}
/*
* (non-Javadoc)
*
* @see freemind.modes.MindMapNode#addHook(freemind.modes.NodeHook)
*/
public PermanentNodeHook addHook(PermanentNodeHook hook) {
// add then
if (hook == null)
throw new IllegalArgumentException("Added null hook.");
createHooks();
hooks.add(hook);
return hook;
}
public void invokeHook(NodeHook hook) {
// initialize:
hook.startupMapHook();
// the main invocation:
hook.setNode(this);
try {
hook.invoke(this);
} catch (Exception e) {
// FIXME: Do something special here, but in any case, do not add the
// hook
// to the activatedHooks:
freemind.main.Resources.getInstance().logException(e);
return;
}
if (hook instanceof PermanentNodeHook) {
createActivatedHooks();
activatedHooks.add(hook);
} else {
// end of its short life:
hook.shutdownMapHook();
}
}
private void createActivatedHooks() {
if (activatedHooks == null) {
activatedHooks = new HashSet();
}
}
private void createToolTip() {
if (toolTip == null) {
toolTip = new TreeMap();
}
}
private void createHooks() {
if (hooks == null) {
hooks = new Vector();
}
}
private void createStateIcons() {
if (stateIcons == null) {
stateIcons = new TreeMap();
}
}
private void createIcons() {
if (icons == null) {
icons = new Vector();
}
}
/*
* (non-Javadoc)
*
* @see freemind.modes.MindMapNode#getHooks()
*/
public List getHooks() {
if (hooks == null)
return Collections.EMPTY_LIST;
return Collections.unmodifiableList(hooks);
}
/*
* (non-Javadoc)
*
* @see freemind.modes.MindMapNode#getActivatedHooks()
*/
public Collection getActivatedHooks() {
if (activatedHooks == null) {
return Collections.EMPTY_LIST;
}
return Collections.unmodifiableCollection(activatedHooks);
}
/*
* (non-Javadoc)
*
* @see freemind.modes.MindMapNode#removeHook(freemind.modes.NodeHook)
*/
public void removeHook(PermanentNodeHook hook) {
// the order is crucial here: the shutdown method should be able to
// perform "nodeChanged"
// calls without having its own updateNodeHook method to be called
// again.
String name = hook.getName();
createActivatedHooks();
if (activatedHooks.contains(hook)) {
activatedHooks.remove(hook);
if (activatedHooks.size() == 0) {
activatedHooks = null;
}
hook.shutdownMapHook();
}
createHooks();
hooks.remove(hook);
if (hooks.size() == 0)
hooks = null;
logger.fine("Removed hook " + name + " at " + hook + ".");
}
public void removeAllHooks() {
int timeout = getHooks().size() * 2;
while (getHooks().size() > 0 && timeout-- > 0) {
PermanentNodeHook hook = (PermanentNodeHook) getHooks().get(0);
try {
removeHook(hook);
} catch (Exception e) {
freemind.main.Resources.getInstance().logException(e);
}
}
}
/**
*/
public SortedMap getToolTip() {
if (toolTip == null)
return new TreeMap();
;
return Collections.unmodifiableSortedMap(toolTip);
}
/**
*/
public void setToolTip(String key, String string) {
createToolTip();
if (string == null) {
if (toolTip.containsKey(key)) {
toolTip.remove(key);
}
if (toolTip.size() == 0)
toolTip = null;
} else {
toolTip.put(key, string);
}
}
/*
* (non-Javadoc)
*
* @see freemind.modes.MindMapNode#getNodeId()
*/
public String getObjectId(ModeController controller) {
return controller.getNodeID(this);
}
/**
* @param writer
* @param registry
* @param managed_attr =0|1|2
* 0 (default): save to .mm file. (do not save certain attributes, such as node's fold status)
* 1 : save to .mmx file. (only save auxiliary attributes, such as node's fold status)
* 2 : all-in-one .mm file. the default behavior of vanilla freemind.
* @return
*/
public XMLElement save(Writer writer, MindMapLinkRegistry registry,
boolean saveInvisible, boolean saveChildren) throws IOException {
return save(writer, registry, saveInvisible, saveChildren, 0);
}
public XMLElement save(Writer writer, MindMapLinkRegistry registry,
boolean saveInvisible, boolean saveChildren, int managed_attr) throws IOException {
// pre save event to save all contents of the node:
getModeController().firePreSaveEvent(this);
XMLElement node = new XMLElement();
// OSSXP.COM: new line before 'TEXT' attribute.
node.addtoEmAttlist("TEXT");
// OSSXP.COM: according to the managed_attr parameter, save or not save certain keys of this NODE.
switch (managed_attr)
{
case 0:
// Save this node to .mm file without certain attributes.
if(Resources.getInstance().getBoolProperty("wh_save_extra_attrs_in_aux_file") && Resources.getInstance().getBoolProperty("wh_separate_attr_created")) node.addtoBlackAttlist("CREATED");
if(Resources.getInstance().getBoolProperty("wh_save_extra_attrs_in_aux_file") && Resources.getInstance().getBoolProperty("wh_separate_attr_modified")) node.addtoBlackAttlist("MODIFIED");
break;
case 1:
// Save this node to .mmx file. Only save certain attributes.
node.addtoWhiteAttlist("ID");
if(Resources.getInstance().getBoolProperty("wh_save_extra_attrs_in_aux_file") && Resources.getInstance().getBoolProperty("wh_separate_attr_folded")) node.addtoWhiteAttlist("FOLDED");
if(Resources.getInstance().getBoolProperty("wh_save_extra_attrs_in_aux_file") && Resources.getInstance().getBoolProperty("wh_separate_attr_created")) node.addtoWhiteAttlist("CREATED");
if(Resources.getInstance().getBoolProperty("wh_save_extra_attrs_in_aux_file") && Resources.getInstance().getBoolProperty("wh_separate_attr_modified")) node.addtoWhiteAttlist("MODIFIED");
break;
}
// if (!isNodeClassToBeSaved()) {
node.setName(XMLElementAdapter.XML_NODE);
// } else {
// node.setName(XMLElementAdapter.XML_NODE_CLASS_PREFIX
// + this.getClass().getName());
// }
/** fc, 12.6.2005: XML must not contain any zero characters. */
// OSSXP.COM: not save TEXT attributes in .mmx file
if( node.isInWhiteAttlist("TEXT"))
{
String text = HtmlTools.makeValidXml(this.toString());
if (!HtmlTools.isHtmlNode(text)) {
node.setAttribute(XMLElementAdapter.XML_NODE_TEXT, text);
} else {
// save <content> tag:
XMLElement htmlElement = new XMLElement();
htmlElement.setName(XMLElementAdapter.XML_NODE_XHTML_CONTENT_TAG);
htmlElement.setAttribute(XMLElementAdapter.XML_NODE_XHTML_TYPE_TAG,
XMLElementAdapter.XML_NODE_XHTML_TYPE_NODE);
htmlElement
.setEncodedContent(convertToEncodedContent(getXmlText()));
node.addChild(htmlElement);
}
if (getXmlNoteText() != null) {
XMLElement htmlElement = new XMLElement();
htmlElement.setName(XMLElementAdapter.XML_NODE_XHTML_CONTENT_TAG);
htmlElement.setAttribute(XMLElementAdapter.XML_NODE_XHTML_TYPE_TAG,
XMLElementAdapter.XML_NODE_XHTML_TYPE_NOTE);
htmlElement
.setEncodedContent(convertToEncodedContent(getXmlNoteText()));
node.addChild(htmlElement);
}
}
// save additional info:
if (getAdditionalInfo() != null) {
node.setAttribute(XMLElementAdapter.XML_NODE_ENCRYPTED_CONTENT,
getAdditionalInfo());
}
// ((MindMapEdgeModel)getEdge()).save(doc,node);
// OSSXP.COM: not save EDGE in .mmx
if( node.isInWhiteAttlist("EDGE"))
{
XMLElement edge = (getEdge()).save();
if (edge != null) {
node.addChild(edge);
}
}
// OSSXP.COM: not save CLOUD in .mmx
if( node.isInWhiteAttlist("CLOUD"))
{
if (getCloud() != null) {
XMLElement cloud = (getCloud()).save();
node.addChild(cloud);
}
}
// OSSXP.COM: not save ARROWLINK in .mmx
if( node.isInWhiteAttlist("ARROWLINK"))
{
Vector linkVector = registry.getAllLinksFromMe(this);
for (int i = 0; i < linkVector.size(); ++i) {
if (linkVector.get(i) instanceof ArrowLinkAdapter) {
XMLElement arrowLinkElement = ((ArrowLinkAdapter) linkVector
.get(i)).save();
node.addChild(arrowLinkElement);
}
}
// virtual link targets:
Vector targetVector = registry.getAllLinksIntoMe(this);
for (int i = 0; i < targetVector.size(); ++i) {
if (targetVector.get(i) instanceof ArrowLinkAdapter) {
XMLElement arrowLinkTargetElement = ((ArrowLinkAdapter) targetVector
.get(i)).createArrowLinkTarget(registry).save();
node.addChild(arrowLinkTargetElement);
}
}
}
// OSSXP.COM: set FOLDED status of all nodes in .mm file to "true". Preserve orignal status in .mmx file.
switch (managed_attr)
{
case 0:
// Save this node to .mm file without certain attributes.
if(Resources.getInstance().getBoolProperty("wh_save_extra_attrs_in_aux_file") && Resources.getInstance().getBoolProperty("wh_separate_attr_folded")) {
if (!isRoot() && !isLeaf()) {
node.setAttribute("FOLDED","true");
}
break;
}
case 1:
case 2:
default:
if (isFolded()) {
node.setAttribute("FOLDED","true");
} else {
node.setAttribute("FOLDED","false");
}
}
// fc, 17.12.2003: Remove the left/right bug.
// VVV save if and only if parent is root.
if (!(isRoot()) && (getParentNode().isRoot())) {
node.setAttribute("POSITION", isLeft() ? "left" : "right");
}
// the id is used, if there is a local hyperlink pointing to me or a
// real link.
String label = registry.getLabel(this);
if (!sSaveOnlyIntrinsicallyNeededIds
|| (registry.isTargetOfLocalHyperlinks(label) || (registry
.getAllLinksIntoMe(this).size() > 0))) {
if (label != null) {
node.setAttribute("ID", label);
}
}
if (color != null) {
node.setAttribute("COLOR", Tools.colorToXml(getColor()));
}
// new background color.
if (getBackgroundColor() != null) {
node.setAttribute("BACKGROUND_COLOR",
Tools.colorToXml(getBackgroundColor()));
}
if (style != null) {
node.setAttribute("STYLE", this.getStyle());
}
// ^ Here cannot be just getStyle() without super. This is because
// getStyle's style depends on folded / unfolded. For example, when
// real style is fork and node is folded, getStyle returns
// MindMapNode.STYLE_BUBBLE, which is not what we want to save.
// layout
if (vGap != VGAP) {
node.setAttribute("VGAP", Integer.toString(vGap));
}
if (hGap != HGAP) {
node.setAttribute("HGAP", Integer.toString(hGap));
}
if (shiftY != 0) {
node.setAttribute("VSHIFT", Integer.toString(shiftY));
}
// link
if (getLink() != null) {
node.setAttribute("LINK", getLink());
}
// history information, fc, 11.4.2005
if (historyInformation != null) {
node.setAttribute(XMLElementAdapter.XML_NODE_HISTORY_CREATED_AT,
Tools.dateToString(getHistoryInformation().getCreatedAt()));
node.setAttribute(
XMLElementAdapter.XML_NODE_HISTORY_LAST_MODIFIED_AT, Tools
.dateToString(getHistoryInformation()
.getLastModifiedAt()));
}
// font
// OSSXP.COM: not save FONT in .mmx
if( node.isInWhiteAttlist("FONT"))
{
if (font != null) {
XMLElement fontElement = new XMLElement();
fontElement.setName("font");
if (font != null) {
fontElement.setAttribute("NAME", font.getFamily());
}
if (font.getSize() != 0) {
fontElement.setAttribute("SIZE",
Integer.toString(font.getSize()));
}
if (isBold()) {
fontElement.setAttribute("BOLD", "true");
}
if (isItalic()) {
fontElement.setAttribute("ITALIC", "true");
}
if (isUnderlined()) {
fontElement.setAttribute("UNDERLINE", "true");
}
node.addChild(fontElement);
}
}
// OSSXP.COM: not save ICON in .mmx
if( node.isInWhiteAttlist("ICON"))
{
for (int i = 0; i < getIcons().size(); ++i) {
XMLElement iconElement = new XMLElement();
iconElement.setName("icon");
iconElement.setAttribute("BUILTIN",
((MindIcon) getIcons().get(i)).getName());
node.addChild(iconElement);
}
}
// OSSXP.COM: not save HOOK in .mmx
if( node.isInWhiteAttlist("HOOK"))
{
for (Iterator i = getActivatedHooks().iterator(); i.hasNext();) {
PermanentNodeHook permHook = (PermanentNodeHook) i.next();
if (permHook instanceof DontSaveMarker) {
continue;
}
XMLElement hookElement = new XMLElement();
hookElement.setName("hook");
permHook.save(hookElement);
node.addChild(hookElement);
}
}
// OSSXP.COM: not save ATTRIBUTE in .mmx
if( node.isInWhiteAttlist("ATTRIBUTE"))
{
attributes.save(node);
}
if (saveChildren && childrenUnfolded().hasNext()) {
node.writeWithoutClosingTag(writer);
// recursive
saveChildren(writer, registry, this, saveInvisible, managed_attr);
node.writeClosingTag(writer);
} else {
node.write(writer);
}
return node;
}
public static String convertToEncodedContent(String xmlText2) {
String replace = HtmlTools.makeValidXml(xmlText2);
return HtmlTools.unicodeToHTMLUnicodeEntity(replace, true);
}
public ModeController getModeController() {
return map.getModeController();
}
// OSSXP.COM: PARAM managed_attr, controls whether or not save certain attrs (such as nodes's folded status) in .mm files.
private void saveChildren(Writer writer, MindMapLinkRegistry registry,
NodeAdapter node, boolean saveHidden) throws IOException {
saveChildren(writer, registry, node, saveHidden, 0);
}
private void saveChildren(Writer writer, MindMapLinkRegistry registry,
NodeAdapter node, boolean saveHidden,
int managed_attr) throws IOException {
for (ListIterator e = node.childrenUnfolded(); e.hasNext();) {
NodeAdapter child = (NodeAdapter) e.next();
if (saveHidden || child.isVisible())
child.save(writer, registry, saveHidden, true, managed_attr);
else
saveChildren(writer, registry, child, saveHidden, managed_attr);
}
}
public int getShiftY() {
return shiftY;
}
public boolean hasExactlyOneVisibleChild() {
int count = 0;
for (ListIterator i = childrenUnfolded(); i.hasNext();) {
if (((MindMapNode) i.next()).isVisible())
count++;
if (count == 2)
return false;
}
return count == 1;
}
public boolean hasVisibleChilds() {
for (ListIterator i = childrenUnfolded(); i.hasNext();) {
if (((MindMapNode) i.next()).isVisible())
return true;
}
return false;
}
public int calcShiftY() {
try {
// return 0;
return shiftY + (parent.hasExactlyOneVisibleChild() ? SHIFT : 0);
} catch (NullPointerException e) {
return 0;
}
}
/**
* @param shiftY
* The shiftY to set.
*/
public void setShiftY(int shiftY) {
this.shiftY = shiftY;
}
/**
*
*/
public void setAdditionalInfo(String info) {
}
public String getAdditionalInfo() {
return null;
}
/** This method must be synchronized as the TreeMap isn't. */
public synchronized void setStateIcon(String key, ImageIcon icon) {
// logger.info("Set state of key:"+key+", icon "+icon);
createStateIcons();
if (icon != null) {
stateIcons.put(key, icon);
getMap().getRegistry().addIcon(MindIcon.factory(key, icon));
} else if (stateIcons.containsKey(key)) {
stateIcons.remove(key);
}
if (stateIcons.size() == 0)
stateIcons = null;
}
public Map getStateIcons() {
if (stateIcons == null)
return Collections.EMPTY_MAP;
return Collections.unmodifiableSortedMap(stateIcons);
}
public HistoryInformation getHistoryInformation() {
return historyInformation;
}
public void setHistoryInformation(HistoryInformation historyInformation) {
this.historyInformation = historyInformation;
}
public int getHGap() {
return hGap;
}
public void setHGap(int gap) {
// hGap = Math.max(HGAP, gap);
hGap = gap;
}
public int getVGap() {
return vGap;
}
public void setVGap(int gap) {
vGap = Math.max(gap, 0);
}
public boolean isVisible() {
Filter filter = getMap().getFilter();
return filter == null || filter.isVisible(this);
}
public NodeAttributeTableModel getAttributes() {
return attributes;
}
public void createAttributeTableModel() {
if (attributes == EMTPY_ATTRIBUTES) {
attributes = new NodeAttributeTableModel(this);
if (views == null) {
return;
}
final Iterator iterator = views.iterator();
while (iterator.hasNext()) {
NodeView view = (NodeView) iterator.next();
view.createAttributeView();
}
}
}
public int getAttributeTableLength() {
return attributes.getRowCount();
}
public Attribute getAttribute(int pPosition) {
return new Attribute(attributes.getAttribute(pPosition));
}
public List getAttributeKeyList() {
Vector returnValue = new Vector();
if (attributes != null) {
for (Iterator iter = attributes.getAttributes().iterator(); iter
.hasNext();) {
Attribute attr = (Attribute) iter.next();
returnValue.add(attr.getName());
}
}
return returnValue;
}
public int getAttributePosition(String pKey) {
if (pKey == null)
return -1;
int pos = 0;
for (Iterator iter = attributes.getAttributes().iterator(); iter
.hasNext();) {
Attribute attr = (Attribute) iter.next();
if (pKey.equals(attr.getName())) {
return pos;
}
pos++;
}
return -1;
}
public String getAttribute(String pKey) {
int attributePosition = getAttributePosition(pKey);
if (attributePosition < 0) {
return null;
}
return getAttribute(attributePosition).getValue();
}
public void setAttribute(int pPosition, Attribute pAttribute) {
createAttributeTableModel();
attributes.setName(pPosition, pAttribute.getName());
attributes.setValue(pPosition, pAttribute.getValue());
}
EventListenerList listenerList = new EventListenerList();
public void addTreeModelListener(TreeModelListener l) {
listenerList.add(TreeModelListener.class, l);
}
public void removeTreeModelListener(TreeModelListener l) {
listenerList.remove(TreeModelListener.class, l);
}
public EventListenerList getListeners() {
return listenerList;
}
/*
* (non-Javadoc)
*
* @see
* freemind.modes.MindMapNode#acceptViewVisitor(freemind.view.mindmapview
* .NodeViewVisitor)
*/
public void acceptViewVisitor(NodeViewVisitor visitor) {
final Iterator iterator = views.iterator();
while (iterator.hasNext()) {
visitor.visit((NodeView) iterator.next());
}
}
//
// Events
//
}