Major cleanup of Utils class.
[trakem2.git] / ini / trakem2 / tree / ProjectTree.java
blob37f015c3e866d6fe194a8c07b7c8be15606d8031
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;
26 import ij.gui.GenericDialog;
27 import ini.trakem2.persistence.DBObject;
28 import ini.trakem2.ControlWindow;
29 import ini.trakem2.Project;
30 import ini.trakem2.display.*;
31 import ini.trakem2.utils.IJError;
32 import ini.trakem2.utils.Render;
33 import ini.trakem2.utils.Utils;
34 import ini.trakem2.utils.Bureaucrat;
35 import ini.trakem2.utils.Worker;
36 import ini.trakem2.utils.Dispatcher;
38 import java.awt.Color;
39 import java.awt.Event;
40 import java.awt.event.KeyEvent;
41 import javax.swing.KeyStroke;
42 import javax.swing.JPopupMenu;
43 import javax.swing.JMenuItem;
44 import javax.swing.JMenu;
45 import java.awt.event.MouseEvent;
46 import java.awt.event.MouseListener;
47 import java.awt.event.ActionEvent;
48 import java.awt.event.ActionListener;
49 import javax.swing.tree.*;
50 import java.util.HashSet;
51 import java.util.HashMap;
52 import java.util.Iterator;
53 import java.util.ArrayList;
54 import java.util.List;
55 import java.util.Collection;
56 import java.util.Enumeration;
57 import java.util.Set;
58 import java.util.Hashtable;
59 import java.util.Collections;
60 import java.io.File;
62 /** A class to hold a tree of Thing nodes */
63 public final class ProjectTree extends DNDTree implements MouseListener, ActionListener {
66 static {
67 System.setProperty("j3d.noOffScreen", "true");
71 private DefaultMutableTreeNode selected_node = null;
73 public ProjectTree(Project project, ProjectThing project_thing) {
74 super(project, DNDTree.makeNode(project_thing), new Color(240,230,255)); // new Color(185,156,255));
75 setEditable(false); // the titles
76 addMouseListener(this);
77 addTreeExpansionListener(this);
80 /** Get a custom, context-sensitive popup menu for the selected node. */
81 private JPopupMenu getPopupMenu(DefaultMutableTreeNode node) {
82 Object ob = node.getUserObject();
83 ProjectThing thing = null;
84 if (ob instanceof ProjectThing) {
85 thing = (ProjectThing)ob;
86 } else {
87 return null;
89 // context-sensitive popup
90 JMenuItem[] items = thing.getPopupItems(this);
91 if (0 == items.length) return null;
92 JPopupMenu popup = new JPopupMenu();
93 for (int i=0; i<items.length; i++) {
94 popup.add(items[i]);
96 JMenu node_menu = new JMenu("Node");
97 JMenuItem item = new JMenuItem("Move up"); item.addActionListener(this); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0, true)); node_menu.add(item);
98 item = new JMenuItem("Move down"); item.addActionListener(this); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0, true)); node_menu.add(item);
100 popup.add(node_menu);
101 return popup;
104 public void mousePressed(final MouseEvent me) {
105 super.dispatcher.execSwing(new Runnable() { public void run() {
106 if (!me.getSource().equals(ProjectTree.this) || !Project.getInstance(ProjectTree.this).isInputEnabled()) {
107 return;
109 final int x = me.getX();
110 final int y = me.getY();
111 // find the node and set it selected
112 final TreePath path = getPathForLocation(x, y);
113 if (null == path) {
114 return;
116 ProjectTree.this.setSelectionPath(path);
117 selected_node = (DefaultMutableTreeNode)path.getLastPathComponent();
119 if (2 == me.getClickCount() && !me.isPopupTrigger() && MouseEvent.BUTTON1 == me.getButton()) {
120 // show in the front Display
121 if (null == selected_node) return;
122 Object obt = selected_node.getUserObject();
123 if (!(obt instanceof ProjectThing)) return;
124 ProjectThing thing = (ProjectThing)obt;
125 thing.setVisible(true);
126 Object obd = thing.getObject();
127 if (obd instanceof Displayable) {
128 // additionaly, get the front Display (or make a new one if none) and show in it the layer in which the Displayable object is contained.
129 Displayable displ = (Displayable)obd;
130 Display.showCentered(displ.getLayer(), displ, true, me.isShiftDown());
132 return;
133 } else if (me.isPopupTrigger() || (ij.IJ.isMacOSX() && 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?
134 JPopupMenu popup = getPopupMenu(selected_node);
135 if (null == popup) return;
136 popup.show(ProjectTree.this, x, y);
137 return;
139 }});
142 public void rename(ProjectThing thing) {
143 Object ob = thing.getObject();
144 String old_title = null;
145 if (null == ob) old_title = thing.getType();
146 else if (ob instanceof DBObject) old_title = ((DBObject)ob).getTitle();
147 else old_title = ob.toString();
148 GenericDialog gd = ControlWindow.makeGenericDialog("New name");
149 gd.addMessage("Old name: " + old_title);
150 gd.addStringField("New name: ", old_title);
151 gd.showDialog();
152 if (gd.wasCanceled()) return;
153 String title = gd.getNextString();
154 title = title.replace('"', '\'').trim(); // avoid XML problems - could also replace by double '', then replace again by " when reading.
155 thing.setTitle(title);
156 this.updateUILater();
160 public void mouseDragged(MouseEvent me) { }
161 public void mouseReleased(MouseEvent me) { }
162 public void mouseEntered(MouseEvent me) { }
163 public void mouseExited(MouseEvent me) { }
164 public void mouseClicked(MouseEvent me) { }
166 public void actionPerformed(final ActionEvent ae) {
167 if (!Project.getInstance(this).isInputEnabled()) return;
168 super.dispatcher.exec(new Runnable() { public void run() {
169 try {
170 if (null == selected_node) return;
171 final Object ob = selected_node.getUserObject();
172 if (!(ob instanceof ProjectThing)) return;
173 final ProjectThing thing = (ProjectThing)ob;
174 int i_position = 0;
175 String command = ae.getActionCommand();
176 final Object obd = thing.getObject();
178 if (command.startsWith("new ") || command.equals("Duplicate")) {
179 ProjectThing new_thing = null;
180 if (command.startsWith("new ")) {
181 new_thing = thing.createChild(command.substring(4)); // if it's a Displayable, it will be added to whatever layer is in the front Display
182 } else if (command.equals("Duplicate")) { // just to keep myself from screwing in the future
183 if (Project.isBasicType(thing.getType()) && null != thing.getParent()) {
184 new_thing = ((ProjectThing)thing.getParent()).createClonedChild(thing);
186 // adjust parent
187 selected_node = (DefaultMutableTreeNode)selected_node.getParent();
189 //add it to the tree
190 if (null != new_thing) {
191 DefaultMutableTreeNode new_node = new DefaultMutableTreeNode(new_thing);
192 ((DefaultTreeModel)ProjectTree.this.getModel()).insertNodeInto(new_node, selected_node, i_position);
193 TreePath treePath = new TreePath(new_node.getPath());
194 ProjectTree.this.scrollPathToVisible(treePath);
195 ProjectTree.this.setSelectionPath(treePath);
197 // bring the display to front
198 if (new_thing.getObject() instanceof Displayable) {
199 Display.getFront().getFrame().toFront();
201 } else if (command.equals("many...")) {
202 ArrayList children = thing.getTemplate().getChildren();
203 if (null == children || 0 == children.size()) return;
204 String[] cn = new String[children.size()];
205 int i=0;
206 for (Iterator it = children.iterator(); it.hasNext(); ) {
207 cn[i] = ((TemplateThing)it.next()).getType();
208 i++;
210 GenericDialog gd = new GenericDialog("Add many children");
211 gd.addNumericField("Amount: ", 1, 0);
212 gd.addChoice("New child: ", cn, cn[0]);
213 gd.addCheckbox("Recursive", true);
214 gd.showDialog();
215 if (gd.wasCanceled()) return;
216 int amount = (int)gd.getNextNumber();
217 if (amount < 1) {
218 Utils.showMessage("Makes no sense to create less than 1 child!");
219 return;
221 final ArrayList nc = thing.createChildren(cn[gd.getNextChoiceIndex()], amount, gd.getNextBoolean());
222 addLeafs((ArrayList<Thing>)nc);
223 } else if (command.equals("Unhide")) {
224 thing.setVisible(true);
225 } else if (command.equals("Select in display")) {
226 boolean shift_down = 0 != (ae.getModifiers() & ActionEvent.SHIFT_MASK);
227 selectInDisplay(thing, shift_down);
228 } else if (command.equals("Identify...")) {
229 // for pipes only for now
230 if (!(obd instanceof Line3D)) return;
231 ini.trakem2.vector.Compare.findSimilar((Line3D)obd);
232 } else if (command.equals("Identify with axes...")) {
233 if (!(obd instanceof Line3D)) return;
234 if (Project.getProjects().size() < 2) {
235 Utils.showMessage("You need at least two projects open:\n-A reference project\n-The current project with the pipe to identify");
236 return;
238 ini.trakem2.vector.Compare.findSimilarWithAxes((Line3D)obd);
239 } else if (command.equals("Show centered in Display")) {
240 if (obd instanceof Displayable) {
241 Displayable displ = (Displayable)obd;
242 Display.showCentered(displ.getLayer(), displ, true, 0 != (ae.getModifiers() & ActionEvent.SHIFT_MASK));
244 } else if (command.equals("Show in 3D")) {
245 ini.trakem2.display.Display3D.showAndResetView(thing);
246 } else if (command.equals("Hide")) {
247 // find all Thing objects in this subtree starting at Thing and hide their Displayable objects.
248 thing.setVisible(false);
249 } else if (command.equals("Delete...")) {
250 remove(true, thing, selected_node);
251 return;
252 } else if (command.equals("Rename...")) {
253 //if (!Project.isBasicType(thing.getType())) {
254 rename(thing);
256 } else if (command.equals("Measure")) {
257 // block displays while measuring
258 Bureaucrat.createAndStart(new Worker("Measuring") { public void run() {
259 startedWorking();
260 try {
261 thing.measure();
262 } catch (Throwable e) {
263 IJError.print(e);
264 } finally {
265 finishedWorking();
267 }}, thing.getProject());
268 }/* else if (command.equals("Export 3D...")) {
269 GenericDialog gd = ControlWindow.makeGenericDialog("Export 3D");
270 String[] choice = new String[]{".svg [preserves links and hierarchical grouping]", ".shapes [limited to one profile per layer per profile_list]"};
271 gd.addChoice("Export to: ", choice, choice[0]);
272 gd.addNumericField("Z scaling: ", 1, 2);
273 gd.showDialog();
274 if (gd.wasCanceled()) return;
275 double z_scale = gd.getNextNumber();
276 switch (gd.getNextChoiceIndex()) {
277 case 0:
278 Render.exportSVG(thing, z_scale);
279 break;
280 case 1:
281 new Render(thing).save(z_scale);
282 break;
284 }*/ else if (command.equals("Export project...") || command.equals("Save as...")) { // "Save as..." is for a FS project
285 Utils.log2("Calling export project at " + System.currentTimeMillis());
286 thing.getProject().getLoader().saveAs(thing.getProject());
287 } else if (command.equals("Save")) {
288 // overwrite the xml file of a FSProject
289 // Just do the same as in "Save as..." but without saving the images and overwritting the XML file without asking.
290 thing.getProject().getLoader().save(thing.getProject());
291 } else if (command.equals("Info")) {
292 showInfo(thing);
293 return;
294 } else if (command.equals("Move up")) {
295 move(selected_node, -1);
296 } else if (command.equals("Move down")) {
297 move(selected_node, 1);
298 } else {
299 Utils.log("ProjectTree.actionPerformed: don't know what to do with the command: " + command);
300 return;
302 } catch (Exception e) {
303 IJError.print(e);
305 }});
308 /** Remove the node, its Thing and the object hold by the thing from the database. */
309 public void remove(DefaultMutableTreeNode node, boolean check, boolean remove_empty_parents, int levels) {
310 Object uob = node.getUserObject();
311 if (!(uob instanceof ProjectThing)) return;
312 ProjectThing thing = (ProjectThing)uob;
313 Display3D.remove(thing);
314 ProjectThing parent = (ProjectThing)thing.getParent();
315 if (!thing.remove(check)) return;
316 ((DefaultTreeModel)this.getModel()).removeNodeFromParent(node);
317 if (remove_empty_parents) removeProjectThingLadder(parent, levels);
318 this.updateUILater();
321 /** Recursive as long as levels is above zero. Levels defines the number of possibly emtpy parent levels to remove. */
322 public void removeProjectThingLadder(ProjectThing lowest, int levels) {
323 if (0 == levels) return;
324 if (lowest.getType().toLowerCase().equals("project")) return; // avoid Project node
325 // check if empty
326 if (null == lowest.getChildren() || 0 == lowest.getChildren().size()) {
327 ProjectThing parent = (ProjectThing)lowest.getParent();
328 if (!lowest.remove(false)) {
329 Utils.log("Failed to remove parent in the ladder: " + lowest);
330 return;
332 // the node
333 DefaultMutableTreeNode node = DNDTree.findNode(lowest, this);
334 ((DefaultTreeModel)this.getModel()).removeNodeFromParent(node);
335 removeProjectThingLadder(parent, levels--);
339 /** Implements the "Duplicate, link and send to next/previous layer" functionality. The 'position' can be zero (before) or 1 (after). The profile has no layer assigned. */
340 public Profile duplicateChild(Profile original, int position, Layer layer) {
341 Utils.log2("ProjectTree: Called duplicateChild " + System.currentTimeMillis() + " for original id = " + original.getId() + " at position " + position);
342 // find the Thing that holds it
343 DefaultMutableTreeNode root = (DefaultMutableTreeNode)this.getModel().getRoot();
344 ProjectThing root_thing = (ProjectThing)root.getUserObject();
345 Thing child = root_thing.findChild(original);
346 if (null == child) {
347 Utils.log("ProjectTree.duplicateChild: node not found for original " + original);
348 return null;
350 Profile copy = (Profile)original.clone();
351 copy.setLayer(layer); // for the Z ordering
352 ProjectThing new_thing = null;
353 try {
354 new_thing = new ProjectThing(((ProjectThing)child.getParent()).getChildTemplate(child.getType()), original.getProject(), copy);
355 } catch (Exception e) {
356 IJError.print(e);
357 return null;
359 DefaultMutableTreeNode child_node = (DefaultMutableTreeNode)findNode(child, this);
360 DefaultMutableTreeNode parent_node = (DefaultMutableTreeNode)child_node.getParent();
361 ProjectThing parent_thing = (ProjectThing)parent_node.getUserObject();
362 //sanity check:
363 if (position < 0) position = 0;
364 else if (position > 1) position = 1;
365 int index = parent_node.getIndex(child_node) + position;
366 if (index < 0) index = 0;
367 if (index > parent_node.getChildCount()) index = parent_node.getChildCount() -1;
368 if (!parent_thing.addChild(new_thing, index)) return null;
369 DefaultMutableTreeNode new_node = new DefaultMutableTreeNode(new_thing);
370 ((DefaultTreeModel)this.getModel()).insertNodeInto(new_node, parent_node, index /*parent_node.getIndex(child_node) + position*/);
371 // relist properly the nodes
372 updateList(parent_node);
373 TreePath treePath = new TreePath(new_node.getPath());
374 this.scrollPathToVisible(treePath);
375 this.setSelectionPath(treePath);
377 return copy;
380 public void destroy() {
381 super.destroy();
382 this.selected_node = null;
385 /* // makes no sense, because there may be multiple projects open and thus the viewport and position may interfere with each other across multiple projects. Saving the collapsed node state suffices.
386 public void exportXML(final StringBuffer sb_body, String indent, JScrollPane jsp) {
387 Point p = jsp.getViewport().getViewPosition();
388 Dimension d = jsp.getSize(null);
389 sb_body.append(indent).append("<t2_tree")
390 .append(" width=\"").append(d.width).append('\"')
391 .append(" height=\"").append(d.height).append('\"')
392 .append(" viewport_x=\"").append(p.e).append('\"')
393 .append(" viewport_y=\"").append(p.y).append('\"')
395 sb_body.append("\n").append(indent).append("</t2_tree>\n");
399 /** Creates a new node of basic type for each AreaList, Ball, Pipe or Polyline present in the ArrayList. Other elements are ignored. */
400 public void insertSegmentations(final Project project, final Collection al) {
401 final TemplateThing tt_root = (TemplateThing)project.getTemplateTree().getRoot().getUserObject();
402 // create a new abstract node called "imported_segmentations", if not there
403 final String imported_labels = "imported_labels";
404 if (!project.typeExists(imported_labels)) {
405 // create it
406 TemplateThing tet = new TemplateThing(imported_labels, project); // yes I know I should check for the project of each Displayable in the ArrayList
407 project.addUniqueType(tet);
408 DefaultMutableTreeNode root = project.getTemplateTree().getRoot();
409 tt_root.addChild(tet);
410 DefaultMutableTreeNode child_node = addChild(tet, root);
411 DNDTree.expandNode(project.getTemplateTree(), child_node);
412 // JTree is serious pain
414 TemplateThing tt_is = project.getTemplateThing(imported_labels); // it's the same as 'tet' above, unless it existed
415 // create a project node from "imported_segmentations" template under a new top node
416 DefaultMutableTreeNode project_node = project.getProjectTree().getRoot();
417 ProjectThing project_pt = (ProjectThing)project_node.getUserObject();
418 ProjectThing ct = project_pt.createChild(tt_root.getType());
419 DefaultMutableTreeNode ctn = addChild(ct, project_node);
420 ProjectThing pt_is = ct.createChild(imported_labels);
421 DefaultMutableTreeNode node_pt_is = addChild(pt_is, ctn);
422 try {
423 // fails when importing labels from Amira TODO
424 this.scrollPathToVisible(new TreePath(node_pt_is.getPath()));
425 } catch (Exception e) {
426 e.printStackTrace();
429 // now, insert a new ProjectThing if of type AreaList, Ball and/or Pipe under node_child
430 for (Iterator it = al.iterator(); it.hasNext(); ) {
431 Object ob = it.next();
432 TemplateThing tt = null;
433 if (ob instanceof AreaList) {
434 tt = getOrCreateChildTemplateThing(tt_is, "area_list");
435 } else if (ob instanceof Pipe) {
436 tt = getOrCreateChildTemplateThing(tt_is, "pipe");
437 } else if (ob instanceof Polyline) {
438 tt = getOrCreateChildTemplateThing(tt_is, "polyline");
439 } else if (ob instanceof Ball) {
440 tt = getOrCreateChildTemplateThing(tt_is, "ball");
441 } else {
442 Utils.log("insertSegmentations: ignoring " + ob);
443 continue;
445 //Utils.log2("tt is " + tt);
446 try {
447 ProjectThing one = new ProjectThing(tt, project, ob);
448 pt_is.addChild(one);
449 addChild(one, node_pt_is);
450 //Utils.log2("one parent : " + one.getParent());
451 } catch (Exception e) {
452 IJError.print(e);
455 DNDTree.expandNode(this, DNDTree.findNode(pt_is, this));
458 private final TemplateThing getOrCreateChildTemplateThing(TemplateThing parent, String type) {
459 TemplateThing tt = parent.getChildTemplate(type);
460 if (null == tt) {
461 tt = new TemplateThing(type, parent.getProject());
462 tt.getProject().addUniqueType(tt);
463 parent.addChild(tt);
465 return tt;
468 public void keyPressed(final KeyEvent ke) {
469 super.keyPressed(ke);
470 if (ke.isConsumed()) return;
471 super.dispatcher.execSwing(new Runnable() { public void run() {
472 if (!ke.getSource().equals(ProjectTree.this) || !Project.getInstance(ProjectTree.this).isInputEnabled()) {
473 return;
475 // get the first selected node only
476 TreePath path = getSelectionPath();
477 if (null == path) return;
478 DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
479 if (null == node) return;
480 final ProjectThing pt = (ProjectThing)node.getUserObject();
481 if (null == pt) return;
483 int key_code = ke.getKeyCode();
484 switch (key_code) {
485 case KeyEvent.VK_PAGE_UP:
486 move(node, -1);
487 ke.consume(); // in any case
488 break;
489 case KeyEvent.VK_PAGE_DOWN:
490 move(node, 1);
491 ke.consume(); // in any case
492 break;
493 case KeyEvent.VK_F2:
494 rename(pt);
495 ke.consume();
496 break;
498 }});
499 ke.consume();
502 /** Move up (-1) or down (1). */
503 private void move(final DefaultMutableTreeNode node, final int direction) {
504 final ProjectThing pt = (ProjectThing)node.getUserObject();
505 if (null == pt) return;
506 // Move: within the list of children of the parent ProjectThing:
507 if (!pt.move(direction)) return;
508 // If moved, reposition within the list of children
509 final DefaultMutableTreeNode parent_node = (DefaultMutableTreeNode)node.getParent();
510 int index = parent_node.getChildCount() -1;
511 ((DefaultTreeModel)this.getModel()).removeNodeFromParent(node);
512 index = pt.getParent().getChildren().indexOf(pt);
513 ((DefaultTreeModel)this.getModel()).insertNodeInto(node, parent_node, index);
514 // restore selection path
515 final TreePath trp = new TreePath(node.getPath());
516 this.scrollPathToVisible(trp);
517 this.setSelectionPath(trp);
518 this.updateUILater();
521 /** If the given node is null, it will be searched for. */
522 public boolean remove(boolean check, ProjectThing thing, DefaultMutableTreeNode node) {
523 Object obd = thing.getObject();
524 if (obd instanceof Project) return ((Project)obd).remove(check); // shortcut to remove everything regardless.
525 boolean b = thing.remove(true) && removeNode(null != node ? node : findNode(thing, this));
526 // This is a patch: removal from buckets is subtly broken
527 // thing.getProject().getRootLayerSet().recreateBuckets(true);
528 // The true problem is that the offscreen repaint thread sets the DisplayCanvas.al_top list before, not at the end of removing all.
529 // --> actually no: querying the LayerSet buckets for ZDisplayables still returns them, but al_zdispl doesn't have them.
530 thing.getProject().getRootLayerSet().recreateBuckets(true);
531 Display.repaint();
532 return b;
535 public void showInfo(ProjectThing thing) {
536 if (null == thing) return;
537 HashMap<String,ArrayList<ProjectThing>> ht = thing.getByType();
538 ArrayList<String> types = new ArrayList<String>();
539 types.addAll(ht.keySet());
540 Collections.sort(types);
541 StringBuffer sb = new StringBuffer(thing.getNodeInfo());
542 sb.append("\nCounts:\n");
543 for (String type : types) {
544 sb.append(type).append(": ").append(ht.get(type).size()).append('\n');
546 sb.append('\n');
547 sb.append(thing.getInfo().replaceAll("\t", " ")); // TextWindow can't handle tabs
548 new ij.text.TextWindow("Info", sb.toString(), 500, 500);
551 public void selectInDisplay(final ProjectThing thing, final boolean shift_down) {
552 Object obd = thing.getObject();
553 if (obd instanceof Displayable) {
554 Displayable d = (Displayable)obd;
555 if (!d.isVisible()) d.setVisible(true);
556 Display display = Display.getFront(d.getProject());
557 if (null == display) return;
558 display.select(d, shift_down);
559 } else {
560 // select all basic types under this leaf
561 HashSet hs = thing.findBasicTypeChildren();
562 boolean first = true;
563 Display display = null;
564 for (Iterator it = hs.iterator(); it.hasNext(); ) {
565 Object ptob = ((ProjectThing)it.next()).getObject();
566 if (!(ptob instanceof Displayable)) {
567 Utils.log2("Skipping non-Displayable object " + ptob);
568 continue;
570 Displayable d = (Displayable)ptob;
571 if (null == display) {
572 display = Display.getFront(d.getProject());
573 if (null == display) return;
575 if (!d.isVisible()) d.setVisible(true);
576 if (first) {
577 display.select(d, shift_down);
578 first = false;
579 } else {
580 display.select(d, true);
586 /** Finds the node for the elder and adds the sibling next to it, under the same parent. */
587 public DefaultMutableTreeNode addSibling(final Displayable elder, final Displayable sibling) {
588 if (null == elder || null == sibling) return null;
589 if (elder.getProject() != sibling.getProject()) {
590 Utils.log2("Can't mix projects!");
591 return null;
593 DefaultMutableTreeNode enode = DNDTree.findNode2(elder, this);
594 if (null == enode) {
595 Utils.log2("Could not find a tree node for elder " + elder);
596 return null;
598 ProjectThing parent = (ProjectThing)((ProjectThing)enode.getUserObject()).getParent();
599 if (null == parent) {
600 Utils.log2("No parent for elder " + elder);
601 return null;
603 TemplateThing tt = elder.getProject().getTemplateThing(Project.getType(sibling.getClass()));
604 if (null == tt) {
605 Utils.log2("Could not find a template for class " + sibling.getClass());
606 return null;
608 ProjectThing pt;
609 try {
610 pt = new ProjectThing(tt, sibling.getProject(), sibling);
611 } catch (Exception e) {
612 IJError.print(e);
613 return null;
615 parent.addChild(pt);
617 DefaultMutableTreeNode node = new DefaultMutableTreeNode(pt);
618 int index = enode.getParent().getIndex(enode);
619 ((DefaultTreeModel)getModel()).insertNodeInto(node, (DefaultMutableTreeNode)enode.getParent(), index + 1);
620 return node;