/**
TrakEM2 plugin for ImageJ(C).
Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
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 (http://www.gnu.org/licenses/gpl.txt )
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.
You may contact Albert Cardona at acardona at ini.phys.ethz.ch
Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
**/
package ini.trakem2.tree;
import ij.gui.GenericDialog;
import ij.gui.YesNoCancelDialog;
import ini.trakem2.ControlWindow;
import ini.trakem2.Project;
import ini.trakem2.utils.Utils;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.regex.Pattern;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
public final class TemplateTree extends DNDTree implements MouseListener, ActionListener {
private DefaultMutableTreeNode selected_node = null;
private TemplateThing root;
public TemplateTree(Project project, TemplateThing root) {
super(project, DNDTree.makeNode(root, true), new Color(245, 255, 245)); //Color(208, 255, 177));
this.root = root;
setEditable(false); // affects the titles only
addMouseListener(this);
expandAllNodes(this, (DefaultMutableTreeNode)getModel().getRoot());
}
public void mousePressed(MouseEvent me) {
Object source = me.getSource();
if (!source.equals(this) || !project.isInputEnabled()) {
return;
}
// allow right-click only
/*if (!(me.isPopupTrigger() || me.isControlDown() || MouseEvent.BUTTON2 == me.getButton() || 0 != (me.getModifiers() & Event.META_MASK))) { // the last block is from ij.gui.ImageCanvas, aparently to make the right-click work on windows?
return;
}*/
if (!Utils.isPopupTrigger(me)) return;
int x = me.getX();
int y = me.getY();
// find the node and set it selected
TreePath path = getPathForLocation(x, y);
if (null == path) return;
setSelectionPath(path);
this.selected_node = (DefaultMutableTreeNode)path.getLastPathComponent();
final TemplateThing tt = (TemplateThing)selected_node.getUserObject();
String type = tt.getType();
//
JPopupMenu popup = new JPopupMenu();
JMenuItem item;
if (!Project.isBasicType(type) && !tt.isNested()) {
JMenu menu = new JMenu("Add new child");
popup.add(menu);
item = new JMenuItem("new..."); item.addActionListener(this); menu.add(item);
// Add also from other open projects
if (ControlWindow.getProjects().size() > 1) {
menu.addSeparator();
JMenu other = new JMenu("From project...");
menu.add(other);
for (Iterator<Project> itp = ControlWindow.getProjects().iterator(); itp.hasNext(); ) {
final Project pr = (Project) itp.next();
if (root.getProject() == pr) continue;
item = new JMenuItem(pr.toString());
other.add(item);
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
GenericDialog gd = new GenericDialog(pr.toString());
gd.addMessage("Project: " + pr.toString());
final HashMap<String,TemplateThing> hm = pr.getTemplateTree().root.getUniqueTypes(new HashMap<String,TemplateThing>());
final String[] u_types = hm.keySet().toArray(new String[0]);
gd.addChoice("type:", u_types, u_types[0]);
gd.showDialog();
if (gd.wasCanceled()) return;
TemplateThing tt_chosen = hm.get(gd.getNextChoice());
// must solve conflicts!
// Recurse into children: if any type that is not a basic type exists in the target project, ban the operation.
ArrayList al = tt_chosen.collectAllChildren(new ArrayList());
for (Iterator ital = al.iterator(); ital.hasNext(); ) {
TemplateThing child = (TemplateThing) ital.next();
if (root.getProject().typeExists(child.getType()) && !pr.isBasicType(child.getType())) {
Utils.showMessage("Type conflict: cannot add type " + tt_chosen.getType());
return;
}
}
// Else add it, recursive into children
// Target is tt
addCopiesRecursively(tt, tt_chosen);
rebuild(selected_node, true);
}
});
}
}
menu.addSeparator();
String[] ut = tt.getProject().getUniqueTypes();
for (int i=0; i<ut.length; i++) {
item = new JMenuItem(ut[i]); item.addActionListener(this); menu.add(item);
}
}
item = new JMenuItem("Delete..."); item.addActionListener(this); popup.add(item);
if (null == selected_node.getParent()) item.setEnabled(false); //disable deletion of root.
if (!Project.isBasicType(type)) {
item = new JMenuItem("Rename..."); item.addActionListener(this); popup.add(item);
}
popup.addSeparator();
item = new JMenuItem("Export XML template..."); item.addActionListener(this); popup.add(item);
popup.show(this, x, y);
}
/** Source may belong to a different project; a copy of source with the project of target will be added to target as a child. */
private void addCopiesRecursively(final TemplateThing target, final TemplateThing source) {
TemplateThing child = new TemplateThing(source.getType(), target.getProject());
if (target.addChild(child)) {
child.addToDatabase();
target.getProject().addUniqueType(child);
ArrayList children = source.getChildren();
if (null != children) {
for (Iterator it = children.iterator(); it.hasNext(); ) {
addCopiesRecursively(child, (TemplateThing) it.next());
}
}
}
}
public void mouseDragged(MouseEvent me) { }
public void mouseReleased(MouseEvent me) { }
public void mouseEntered(MouseEvent me) { }
public void mouseExited(MouseEvent me) { }
public void mouseClicked(MouseEvent me) { }
public void actionPerformed(ActionEvent ae) {
String command = ae.getActionCommand();
//Utils.log2("command: " + command);
TemplateThing tt = (TemplateThing)selected_node.getUserObject();
if (command.equals("Rename...")) {
final GenericDialog gd = new GenericDialog("Rename");
gd.addStringField("New type name: ", tt.getType(), 40);
gd.showDialog();
if (gd.wasCanceled()) return;
String old_name = tt.getType();
String new_name = gd.getNextString().replace(' ', '_').trim();
if (null == new_name || 0 == new_name.length() || isInvalidTypeName(new_name, false)) {
Utils.showMessage("Unacceptable new name: '" + new_name + "'");///////////
return;
}
renameType(old_name, new_name);
} else if (command.equals("Delete...")) {
// find dependent objects, if any, that have the same type of parent chain
HashSet<ProjectThing> hs = tt.getProject().getRootProjectThing().collectSimilarThings(tt, new HashSet<ProjectThing>());
YesNoCancelDialog yn = ControlWindow.makeYesNoCancelDialog("Remove type?", "Really remove type '" + tt.getType() + "'" + ((null != tt.getChildren() && 0 != tt.getChildren().size()) ? " and its children" : "") + (0 == hs.size() ? "" : " from parent " + tt.getParent().getType() + ",\nand its " + hs.size() + " existing instance" + (1 == hs.size() ? "" : "s") + " in the project tree?"));
if (!yn.yesPressed()) return;
// else, proceed to delete:
//Utils.log("Going to delete TemplateThing: " + tt.getType() + " id: " + tt.getId());
// first, remove the project things
DNDTree project_tree = tt.getProject().getProjectTree();
for (final ProjectThing pt : hs) {
Utils.log("\tDeleting ProjectThing: " + pt + " " + pt.getId());
if (!pt.remove(false)) {
Utils.showMessage("Can't delete ProjectThing " + pt + " " + pt.getId());
}
DefaultMutableTreeNode node = DNDTree.findNode(pt, project_tree);
if (null != node) ((DefaultTreeModel)project_tree.getModel()).removeNodeFromParent(node);
else Utils.log("Can't find a node for PT " + pt + " " + pt.getId());
}
// then, remove the template things that have the same type and parent type as the selected one
HashSet<TemplateThing> hst = root.collectSimilarThings(tt, new HashSet<TemplateThing>());
HashSet hs_same_type = root.collectThingsOfEqualType(tt, new HashSet());
Utils.log2("hs_same_type.size() = " + hs_same_type.size());
for (final TemplateThing tet : hst) {
if (1 != hs_same_type.size() && tet.equals(tet.getProject().getTemplateThing(tet.getType()))) {
// don't delete if this is the primary copy, stored in the project unique types (which should be clones, to avoid this problem)
Utils.log2("avoiding 1");
} else {
Utils.log("\tDeleting TemplateThing: " + tet + " " + tet.getId());
if (!tet.remove(false)) {
Utils.showMessage("Can't delete TemplateThing" + tet + " " + tet.getId());
}
}
// remove the node in any case
DefaultMutableTreeNode node = DNDTree.findNode(tet, this);
if (null != node) ((DefaultTreeModel)this.getModel()).removeNodeFromParent(node);
else Utils.log("Can't find a node for TT " + tet + " " + tet.getId());
}
// finally, find out whether there are any TT of the deleted type in the Project unique collection of TT, and delete it. Considers nested problem: if the deleted TT was a nested one, doesn't delete it from the unique types Hashtable. Also should not delete it if there are other instances of the same TT but under different parents.
if (!tt.isNested() && 1 == hs_same_type.size()) {
tt.getProject().removeUniqueType(tt.getType());
Utils.log2("removing unique type");
} else {
Utils.log2("avoiding 2");
}
// update trees
this.updateUILater();
project_tree.updateUILater();
} else if (command.equals("Export XML template...")) {
/*
GenericDialog gd = ControlWindow.makeGenericDialog("Doc Name");
gd.addMessage("Please provide an XML document type");
gd.addStringField("DOCTYPE: ", "");
gd.showDialog();
if (gd.wasCanceled()) return;
String doctype = gd.getNextString();
if (null == doctype || 0 == doctype.length()) {
Utils.showMessage("Invalid DOCTYPE!");
return;
}
doctype = doctype.replace(' ', '_'); //spaces may not play well in the XML file
*/
//StringBuffer sb = new StringBuffer("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n<!DOCTYPE ");
StringBuilder sb = new StringBuilder("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n");
//sb.append(doctype).append(" [\n");
HashSet hs = new HashSet(); // accumulate ELEMENT and ATTLIST
//StringBuffer sb2 = new StringBuffer();
//root.exportXMLTemplate(sb, sb2, hs, "");
//tt.exportXMLTemplate(sb, sb2, hs, ""); // from the selected one (a subtree unless the selected is the root)
tt.exportDTD(sb, hs, ""); // from the selected one (a subtree unless the selected is the root)
//String xml = sb.append("] >\n\n").toString() + sb2.toString();
Utils.saveToFile(tt.getType(), ".dtd", sb.toString()/*xml*/);
} else {
TemplateThing tet = null;
boolean is_new = false;
String new_child_type = null;
if (command.equals("new...")) {
is_new = true;
// for adding a new child, prevent so in nested types
// ALREADY done, since menus to add a new type don't show up for nested types
GenericDialog gd = ControlWindow.makeGenericDialog("New child");
gd.addStringField("Type name: ", "");
gd.showDialog();
if (gd.wasCanceled()) return;
String new_type = gd.getNextString().toLowerCase(); // TODO WARNING toLowerCase enforced, need to check the TMLHandler
// replace spaces before testing for non-alphanumeric chars
new_type = new_type.replace(' ', '_').trim(); // spaces don't play well in an XML file.
if (isInvalidTypeName(new_type, true)) {
return;
}
if (tt.getProject().typeExists(new_type.toLowerCase())) {
Utils.showMessage("Type '" + new_type + "' exists already.\nSelect it from the contextual menu list\nor choose a different name.");
return;
} else if (Project.isBasicType(new_type.toLowerCase())) {
Utils.showMessage("Type '" + new_type + "' is reserved.\nPlease choose a different name.");
return;
}
//tet = new TemplateThing(new_type, tt.getProject());
//tt.getProject().addUniqueType(tet);
new_child_type = new_type;
} else {
// create from a listed type
tet = tt.getProject().getTemplateThing(command);
if (tt.canHaveAsChild(tet)) {
Utils.log("'" + tt.getType() + "' already contains a child of type '" + command + "'");
return;
} else if (null == tet) {
Utils.log("TemplateTree internal error: no type exists for '" + command + "'");
return;
}
// else add as new
new_child_type = command; //tet = new TemplateThing(command, tt.getProject());
}
// add the new type to the database and to the tree, to all instances that are similar to tt (but not nested)
addNewChildType(tt, new_child_type);
}
}
/** Returns true if @param type conforms with ^[a-zA-Z][a-zA-Z0-9_]*$ */
public boolean isInvalidTypeName(final String type) {
return isInvalidTypeName(type, false);
}
private boolean isInvalidTypeName(final String type, final boolean showmsg) {
final Pattern pat = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]*$", Pattern.CASE_INSENSITIVE);
if (pat.matcher(type).matches()) {
return false;
} else {
if (showmsg) Utils.showMessage("Only alphanumeric characters, underscore, hyphen and space are accepted.\nAnd the name must start with a character.");
return true;
}
}
public void destroy() {
super.destroy();
this.root = null;
this.selected_node = null;
}
/** Recursively create TemplateThing copies and new nodes to fill in the whole subtree of the given parent; nested types will be prevented from being filled.*/
private void fillChildren(final TemplateThing parent, final DefaultMutableTreeNode parent_node) {
TemplateThing parent_full = parent.getProject().getTemplateThing(parent.getType());
if (parent.isNested()) {
//Utils.log2("avoiding nested infinite recursion problem");
return;
}
final ArrayList al_children = parent_full.getChildren();
if (null == al_children) {
//Utils.log2("no children for " + parent_full);
return;
}
for (Iterator it = al_children.iterator(); it.hasNext(); ) {
TemplateThing child = (TemplateThing)it.next();
TemplateThing copy = new TemplateThing(child.getType(), parent.getProject());
parent.addChild(copy);
DefaultMutableTreeNode copy_node = new DefaultMutableTreeNode(copy);
((DefaultTreeModel)this.getModel()).insertNodeInto(copy_node, parent_node, parent_node.getChildCount());
fillChildren(copy, copy_node);
}
}
/** Add a new template thing to an existing ProjectThing, so that new instances of template new_child_type can be added to the ProjectThing pt. */
public TemplateThing addNewChildType(final ProjectThing pt, String new_child_type) {
if (null == pt.getParent() || null == pt.getTemplate()) return null;
TemplateThing tt_parent = pt.getTemplate().getChildTemplate(new_child_type);
if (null != tt_parent) return tt_parent;
// Else create it
return addNewChildType(pt.getTemplate(), new_child_type);
}
/** tt_parent is the parent TemplateThing
* tet_child is the child to add to tt parent, and to insert as child to all nodes that host the tt parent.
*
* Returns the TemplateThing used, either new or a reused-unique-already-existing one. */
public TemplateThing addNewChildType(final TemplateThing tt_parent, String new_child_type) {
// check preconditions
if (null == tt_parent || null == new_child_type) return null;
// fix any potentially dangerous chars for the XML
new_child_type = new_child_type.trim().toLowerCase().replace(' ', '_').replace('-', '_').replace('\n','_').replace('\t','_'); // XML valid
// See if such TemplateThing exists already
TemplateThing tet_child = tt_parent.getProject().getTemplateThing(new_child_type);
boolean is_new = null == tet_child;
// In any case we need a copy to add as a node to the trees
tet_child = new TemplateThing(null == tet_child ? new_child_type : tet_child.getType(), tt_parent.getProject()); // reusing same String
if (is_new) {
tt_parent.getProject().addUniqueType(tet_child);
}
tt_parent.addChild(tet_child);
// add the new type to the database and to the tree, to all instances that are similar to tt (but not nested)
HashSet hs = root.collectThingsOfEqualType(tt_parent, new HashSet());
for (Iterator it = hs.iterator(); it.hasNext(); ) {
TemplateThing tti, ttc;
tti = (TemplateThing)it.next();
if (tti.isNested()) continue;
if (tti.equals(tt_parent)) {
tti = tt_parent; // parent
ttc = tet_child; // child
} else {
ttc = new TemplateThing(tet_child.getType(), tt_parent.getProject());
tti.addChild(ttc);
ttc.addToDatabase();
}
// find the parent
DefaultMutableTreeNode node_parent = DNDTree.findNode(tti, this);
DefaultMutableTreeNode node_child = new DefaultMutableTreeNode(ttc);
// see first if there isn't already one such child
boolean add = true;
for (final Enumeration e = node_parent.children(); e.hasMoreElements(); ) {
DefaultMutableTreeNode nc = (DefaultMutableTreeNode) e.nextElement();
TemplateThing ttnc = (TemplateThing) nc.getUserObject();
if (ttnc.getType().equals(ttc.getType())) {
add = false;
break;
}
}
if (add) {
((DefaultTreeModel)this.getModel()).insertNodeInto(node_child, node_parent, node_parent.getChildCount());
}
Utils.log2("ttc parent: " + ttc.getParent());
Utils.log2("tti is parent: " + (tti == ttc.getParent()));
// generalize the code below to add all children of an exisiting type when adding it as a leaf somewhere else than it's first location
// 1 - find if the new 'tet' is of a type that existed already
if (!is_new) {
// 2 - add new TemplateThing nodes to fill in the whole subtree, preventing nested expansion
//Utils.log2("Calling fillChildren for " + tet);
fillChildren(tet_child, node_child); // recursive
DNDTree.expandAllNodes(this, node_child);
} else {
//Utils.log2("NOT Calling fillChildren for " + tet);
}
}
this.updateUILater();
return tet_child;
}
/** Rename a TemplateThing type from @param old_name to @param new_name.
* If such a new_name already exists, the renaming will not occur and returns false. */
public boolean renameType(final String old_name, String new_name) {
// to lower case!
new_name = new_name.toLowerCase();
Project project = root.getProject();
if (new_name.equals(old_name)) {
return true;
} else if (project.typeExists(new_name)) {
Utils.logAll("Type '" + new_name + "' exists already!");
return false;
}
// process name change in all TemplateThing instances that have it
ArrayList<TemplateThing> al = root.collectAllChildren(new ArrayList<TemplateThing>());
al.add(root);
for (final TemplateThing tet : al) {
//Utils.log("\tchecking " + tet.getType() + " " + tet.getId());
if (tet.getType().equals(old_name)) tet.rename(new_name);
}
// and update the ProjectThing objects in the tree and its dependant Displayable objects in the open Displays
project.getRootProjectThing().updateType(new_name, old_name);
// tell the project about it
project.updateTypeName(old_name, new_name);
// repaint both trees (will update the type names)
updateUILater();
project.getProjectTree().updateUILater();
return true;
}
@Override
protected Thing getRootThing() {
return project.getRootTemplateThing();
}
}