Major cleanup of Utils class.
[trakem2.git] / ini / trakem2 / tree / TemplateTree.java
blobf80a1294d1c5833e145aaf83ca641e1b446556db
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005,2006 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2.tree;
25 import ini.trakem2.Project;
26 import ini.trakem2.ControlWindow;
27 import ini.trakem2.utils.Utils;
29 import java.awt.Color;
30 import java.awt.Component;
31 import java.awt.Event;
32 import java.awt.event.MouseEvent;
33 import java.awt.event.MouseListener;
34 import java.awt.event.ActionEvent;
35 import java.awt.event.ActionListener;
36 import java.util.*;
37 import javax.swing.tree.*;
38 import javax.swing.JPopupMenu;
39 import javax.swing.JMenuItem;
40 import javax.swing.JMenu;
41 import ij.gui.GenericDialog;
42 import ij.gui.YesNoCancelDialog;
43 import java.util.regex.Pattern;
46 public final class TemplateTree extends DNDTree implements MouseListener, ActionListener {
48 private DefaultMutableTreeNode selected_node = null;
49 private TemplateThing root;
51 public TemplateTree(Project project, TemplateThing root) {
52 super(project, DNDTree.makeNode(root, true), new Color(245, 255, 245)); //Color(208, 255, 177));
53 this.root = root;
54 setEditable(false); // affects the titles only
55 addMouseListener(this);
56 expandAllNodes(this, (DefaultMutableTreeNode)getModel().getRoot());
59 public void mousePressed(MouseEvent me) {
60 Object source = me.getSource();
61 if (!source.equals(this) || !Project.getInstance(this).isInputEnabled()) {
62 return;
64 // allow right-click only
65 /*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?
66 return;
67 }*/
68 if (!Utils.isPopupTrigger(me)) return;
69 int x = me.getX();
70 int y = me.getY();
71 // find the node and set it selected
72 TreePath path = getPathForLocation(x, y);
73 if (null == path) return;
74 setSelectionPath(path);
75 this.selected_node = (DefaultMutableTreeNode)path.getLastPathComponent();
76 final TemplateThing tt = (TemplateThing)selected_node.getUserObject();
77 String type = tt.getType();
79 JPopupMenu popup = new JPopupMenu();
80 JMenuItem item;
82 if (!Project.isBasicType(type) && !tt.isNested()) {
83 JMenu menu = new JMenu("Add new child");
84 popup.add(menu);
85 item = new JMenuItem("new..."); item.addActionListener(this); menu.add(item);
87 // Add also from other open projects
88 if (ControlWindow.getProjects().size() > 1) {
89 menu.addSeparator();
90 JMenu other = new JMenu("From project...");
91 menu.add(other);
92 for (Iterator itp = ControlWindow.getProjects().iterator(); itp.hasNext(); ) {
93 final Project pr = (Project) itp.next();
94 if (root.getProject() == pr) continue;
95 item = new JMenuItem(pr.toString());
96 other.add(item);
97 item.addActionListener(new ActionListener() {
98 public void actionPerformed(ActionEvent ae) {
99 GenericDialog gd = new GenericDialog(pr.toString());
100 gd.addMessage("Project: " + pr.toString());
101 final HashMap<String,TemplateThing> hm = pr.getTemplateTree().root.getUniqueTypes(new HashMap<String,TemplateThing>());
102 final String[] u_types = hm.keySet().toArray(new String[0]);
103 gd.addChoice("type:", u_types, u_types[0]);
104 gd.showDialog();
105 if (gd.wasCanceled()) return;
106 TemplateThing tt_chosen = hm.get(gd.getNextChoice());
107 // must solve conflicts!
108 // Recurse into children: if any type that is not a basic type exists in the target project, ban the operation.
109 ArrayList al = tt_chosen.collectAllChildren(new ArrayList());
110 for (Iterator ital = al.iterator(); ital.hasNext(); ) {
111 TemplateThing child = (TemplateThing) ital.next();
112 if (root.getProject().typeExists(child.getType()) && !pr.isBasicType(child.getType())) {
113 Utils.showMessage("Type conflict: cannot add type " + tt_chosen.getType());
114 return;
117 // Else add it, recursive into children
118 // Target is tt
119 addCopiesRecursively(tt, tt_chosen);
120 rebuild(selected_node, true);
125 menu.addSeparator();
126 String[] ut = tt.getProject().getUniqueTypes();
127 for (int i=0; i<ut.length; i++) {
128 item = new JMenuItem(ut[i]); item.addActionListener(this); menu.add(item);
132 item = new JMenuItem("Delete..."); item.addActionListener(this); popup.add(item);
133 if (null == selected_node.getParent()) item.setEnabled(false); //disable deletion of root.
135 if (!Project.isBasicType(type)) {
136 item = new JMenuItem("Rename..."); item.addActionListener(this); popup.add(item);
138 popup.addSeparator();
139 item = new JMenuItem("Export XML template..."); item.addActionListener(this); popup.add(item);
141 popup.show(this, x, y);
144 /** Source may belong to a different project; a copy of source with the project of target will be added to target as a child. */
145 private void addCopiesRecursively(final TemplateThing target, final TemplateThing source) {
146 TemplateThing child = new TemplateThing(source.getType(), target.getProject());
147 if (target.addChild(child)) {
148 child.addToDatabase();
149 target.getProject().addUniqueType(child);
150 ArrayList children = source.getChildren();
151 if (null != children) {
152 for (Iterator it = children.iterator(); it.hasNext(); ) {
153 addCopiesRecursively(child, (TemplateThing) it.next());
159 public void mouseDragged(MouseEvent me) { }
160 public void mouseReleased(MouseEvent me) { }
161 public void mouseEntered(MouseEvent me) { }
162 public void mouseExited(MouseEvent me) { }
163 public void mouseClicked(MouseEvent me) { }
165 public void actionPerformed(ActionEvent ae) {
166 String command = ae.getActionCommand();
167 //Utils.log2("command: " + command);
168 TemplateThing tt = (TemplateThing)selected_node.getUserObject();
170 // Determine whether tt is a nested type
171 Thing parent = tt.getParent();
173 if (command.equals("Rename...")) {
174 final GenericDialog gd = new GenericDialog("Rename");
175 gd.addStringField("New type name: ", tt.getType());
176 gd.showDialog();
177 if (gd.wasCanceled()) return;
178 String old_name = tt.getType();
179 String new_name = gd.getNextString().replace(' ', '_');
180 if (null == new_name || 0 == new_name.length()) {
181 Utils.showMessage("Unacceptable new name: '" + new_name + "'");
182 return;
184 // to lower case!
185 new_name = new_name.toLowerCase();
186 if (new_name.equals(old_name)) {
187 return;
188 } else if (tt.getProject().typeExists(new_name)) {
189 Utils.showMessage("Type '" + new_name + "' exists already.\nChoose a different name.");
190 return;
192 // process name change in all TemplateThing instances that have it
193 ArrayList al = root.collectAllChildren(new ArrayList());
194 al.add(root);
195 for (Iterator it = al.iterator(); it.hasNext(); ) {
196 TemplateThing tet = (TemplateThing)it.next();
197 //Utils.log("\tchecking " + tet.getType() + " " + tet.getId());
198 if (tet.getType().equals(old_name)) tet.rename(new_name);
200 // and update the ProjectThing objects in the tree and its dependant Displayable objects in the open Displays
201 tt.getProject().getRootProjectThing().updateType(new_name, old_name);
202 // tell the project about it
203 tt.getProject().updateTypeName(old_name, new_name);
204 // repaint both trees (will update the type names)
205 updateUILater();
206 tt.getProject().getProjectTree().updateUILater();
207 } else if (command.equals("Delete...")) {
208 // find dependent objects, if any, that have the same type of parent chain
209 HashSet hs = tt.getProject().getRootProjectThing().collectSimilarThings(tt, new HashSet());
210 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?"));
211 if (!yn.yesPressed()) return;
212 // else, proceed to delete:
213 //Utils.log("Going to delete TemplateThing: " + tt.getType() + " id: " + tt.getId());
214 // first, remove the project things
215 DNDTree project_tree = tt.getProject().getProjectTree();
216 for (Iterator it = hs.iterator(); it.hasNext(); ) {
217 ProjectThing pt = (ProjectThing)it.next();
218 Utils.log("\tDeleting ProjectThing: " + pt + " " + pt.getId());
219 if (!pt.remove(false)) {
220 Utils.showMessage("Can't delete ProjectThing " + pt + " " + pt.getId());
222 DefaultMutableTreeNode node = DNDTree.findNode(pt, project_tree);
223 if (null != node) ((DefaultTreeModel)project_tree.getModel()).removeNodeFromParent(node);
224 else Utils.log("Can't find a node for PT " + pt + " " + pt.getId());
226 // then, remove the template things that have the same type and parent type as the selected one
227 hs = root.collectSimilarThings(tt, new HashSet());
228 HashSet hs_same_type = root.collectThingsOfEqualType(tt, new HashSet());
229 Utils.log2("hs_same_type.size() = " + hs_same_type.size());
230 for (Iterator it = hs.iterator(); it.hasNext(); ) {
231 TemplateThing tet = (TemplateThing)it.next();
232 if (1 != hs_same_type.size() && tet.equals(tet.getProject().getTemplateThing(tet.getType()))) {
233 // don't delete if this is the primary copy, stored in the project unique types (which should be clones, to avoid this problem)
234 Utils.log2("avoiding 1");
235 } else {
236 Utils.log("\tDeleting TemplateThing: " + tet + " " + tet.getId());
237 if (!tet.remove(false)) {
238 Utils.showMessage("Can't delete TemplateThing" + tet + " " + tet.getId());
241 // remove the node in any case
242 DefaultMutableTreeNode node = DNDTree.findNode(tet, this);
243 if (null != node) ((DefaultTreeModel)this.getModel()).removeNodeFromParent(node);
244 else Utils.log("Can't find a node for TT " + tet + " " + tet.getId());
246 // 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.
247 if (!tt.isNested() && 1 == hs_same_type.size()) {
248 tt.getProject().removeUniqueType(tt.getType());
249 Utils.log2("removing unique type");
250 } else {
251 Utils.log2("avoiding 2");
253 // update trees
254 this.updateUILater();
255 project_tree.updateUILater();
256 } else if (command.equals("Export XML template...")) {
258 GenericDialog gd = ControlWindow.makeGenericDialog("Doc Name");
259 gd.addMessage("Please provide an XML document type");
260 gd.addStringField("DOCTYPE: ", "");
261 gd.showDialog();
262 if (gd.wasCanceled()) return;
263 String doctype = gd.getNextString();
264 if (null == doctype || 0 == doctype.length()) {
265 Utils.showMessage("Invalid DOCTYPE!");
266 return;
268 doctype = doctype.replace(' ', '_'); //spaces may not play well in the XML file
270 //StringBuffer sb = new StringBuffer("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n<!DOCTYPE ");
271 StringBuffer sb = new StringBuffer("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n");
272 //sb.append(doctype).append(" [\n");
273 HashSet hs = new HashSet(); // accumulate ELEMENT and ATTLIST
274 //StringBuffer sb2 = new StringBuffer();
275 //root.exportXMLTemplate(sb, sb2, hs, "");
276 //tt.exportXMLTemplate(sb, sb2, hs, ""); // from the selected one (a subtree unless the selected is the root)
277 tt.exportDTD(sb, hs, ""); // from the selected one (a subtree unless the selected is the root)
278 //String xml = sb.append("] >\n\n").toString() + sb2.toString();
279 Utils.saveToFile(tt.getType(), ".dtd", sb.toString()/*xml*/);
280 } else {
281 TemplateThing tet = null;
282 boolean is_new = false;
283 String new_child_type = null;
285 if (command.equals("new...")) {
286 is_new = true;
287 // for adding a new child, prevent so in nested types
288 // ALREADY done, since menus to add a new type don't show up for nested types
289 GenericDialog gd = ControlWindow.makeGenericDialog("New child");
290 gd.addStringField("Type name: ", "");
291 gd.showDialog();
292 if (gd.wasCanceled()) return;
293 String new_type = gd.getNextString().toLowerCase(); // TODO WARNING toLowerCase enforced, need to check the TMLHandler
294 if (tt.getProject().typeExists(new_type.toLowerCase())) {
295 Utils.showMessage("Type '" + new_type + "' exists already.\nSelect it from the contextual menu list\nor choose a different name.");
296 return;
297 } else if (tt.getProject().isBasicType(new_type.toLowerCase())) {
298 Utils.showMessage("Type '" + new_type + "' is reserved.\nPlease choose a different name.");
299 return;
302 // replace spaces before testing for non-alphanumeric chars
303 new_type = new_type.replace(' ', '_'); // spaces don't play well in an XML file.
305 final Pattern pat = Pattern.compile("^.*[^a-zA-Z0-9_-].*$", Pattern.CASE_INSENSITIVE);
306 if (pat.matcher(new_type).matches()) {
307 Utils.showMessage("Only alphanumeric characters, underscore, hyphen and space are accepted.");
308 return;
311 //tet = new TemplateThing(new_type, tt.getProject());
312 //tt.getProject().addUniqueType(tet);
313 new_child_type = new_type;
314 } else {
315 // create from a listed type
316 tet = tt.getProject().getTemplateThing(command);
317 if (tt.canHaveAsChild(tet)) {
318 Utils.log("'" + tt.getType() + "' already contains a child of type '" + command + "'");
319 return;
320 } else if (null == tet) {
321 Utils.log("TemplateTree internal error: no type exists for '" + command + "'");
322 return;
324 // else add as new
325 new_child_type = command; //tet = new TemplateThing(command, tt.getProject());
328 // add the new type to the database and to the tree, to all instances that are similar to tt (but not nested)
329 addNewChildType(tt, new_child_type);
333 public void destroy() {
334 super.destroy();
335 this.root = null;
336 this.selected_node = null;
339 /** 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.*/
340 private void fillChildren(final TemplateThing parent, final DefaultMutableTreeNode parent_node) {
341 TemplateThing parent_full = parent.getProject().getTemplateThing(parent.getType());
342 if (parent.isNested()) {
343 //Utils.log2("avoiding nested infinite recursion problem");
344 return;
346 final ArrayList al_children = parent_full.getChildren();
347 if (null == al_children) {
348 //Utils.log2("no children for " + parent_full);
349 return;
351 for (Iterator it = al_children.iterator(); it.hasNext(); ) {
352 TemplateThing child = (TemplateThing)it.next();
353 TemplateThing copy = new TemplateThing(child.getType(), parent.getProject());
354 parent.addChild(copy);
355 DefaultMutableTreeNode copy_node = new DefaultMutableTreeNode(copy);
356 ((DefaultTreeModel)this.getModel()).insertNodeInto(copy_node, parent_node, parent_node.getChildCount());
357 fillChildren(copy, copy_node);
361 /** 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. */
362 public TemplateThing addNewChildType(final ProjectThing pt, String new_child_type) {
363 if (null == pt.getParent() || null == pt.getTemplate()) return null;
364 TemplateThing tt_parent = pt.getTemplate().getChildTemplate(new_child_type);
365 if (null != tt_parent) return tt_parent;
366 // Else create it
367 return addNewChildType(pt.getTemplate(), new_child_type);
370 /** tt_parent is the parent TemplateThing
371 * tet_child is the child to add to tt parent, and to insert as child to all nodes that host the tt parent.
373 * Returns the TemplateThing used, either new or a reused-unique-already-existing one. */
374 public TemplateThing addNewChildType(final TemplateThing tt_parent, String new_child_type) {
375 // check preconditions
376 if (null == tt_parent || null == new_child_type) return null;
377 // fix any potentially dangerous chars for the XML
378 new_child_type = new_child_type.trim().toLowerCase().replace(' ', '_').replace('-', '_').replace('\n','_').replace('\t','_'); // XML valid
379 // See if such TemplateThing exists already
380 TemplateThing tet_child = tt_parent.getProject().getTemplateThing(new_child_type);
381 boolean is_new = null == tet_child;
382 // In any case we need a copy to add as a node to the trees
383 tet_child = new TemplateThing(null == tet_child ? new_child_type : tet_child.getType(), tt_parent.getProject()); // reusing same String
384 if (is_new) {
385 tt_parent.getProject().addUniqueType(tet_child);
387 tt_parent.addChild(tet_child);
389 // add the new type to the database and to the tree, to all instances that are similar to tt (but not nested)
390 HashSet hs = root.collectThingsOfEqualType(tt_parent, new HashSet());
391 for (Iterator it = hs.iterator(); it.hasNext(); ) {
392 TemplateThing tti, ttc;
393 tti = (TemplateThing)it.next();
394 if (tti.isNested()) continue;
395 if (tti.equals(tt_parent)) {
396 tti = tt_parent; // parent
397 ttc = tet_child; // child
398 } else {
399 ttc = new TemplateThing(tet_child.getType(), tt_parent.getProject());
400 tti.addChild(ttc);
401 ttc.addToDatabase();
403 // find the parent
404 DefaultMutableTreeNode node_parent = DNDTree.findNode(tti, this);
405 DefaultMutableTreeNode node_child = new DefaultMutableTreeNode(ttc);
406 // see first if there isn't already one such child
407 boolean add = true;
408 for (final Enumeration e = node_parent.children(); e.hasMoreElements(); ) {
409 DefaultMutableTreeNode nc = (DefaultMutableTreeNode) e.nextElement();
410 TemplateThing ttnc = (TemplateThing) nc.getUserObject();
411 if (ttnc.getType().equals(ttc.getType())) {
412 add = false;
413 break;
416 if (add) {
417 ((DefaultTreeModel)this.getModel()).insertNodeInto(node_child, node_parent, node_parent.getChildCount());
420 Utils.log2("ttc parent: " + ttc.getParent());
421 Utils.log2("tti is parent: " + (tti == ttc.getParent()));
423 // 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
424 // 1 - find if the new 'tet' is of a type that existed already
425 if (!is_new) {
426 // 2 - add new TemplateThing nodes to fill in the whole subtree, preventing nested expansion
427 //Utils.log2("Calling fillChildren for " + tet);
428 fillChildren(tet_child, node_child); // recursive
429 DNDTree.expandAllNodes(this, node_child);
430 } else {
431 //Utils.log2("NOT Calling fillChildren for " + tet);
434 this.updateUILater();
435 return tet_child;