Fix for excessibe snapshot panel repaints.
[trakem2.git] / ini / trakem2 / Project.java
blob464228fd2d4e7c45b39ac31b9632d869420f7c31
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005 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;
26 import ij.IJ;
27 import ij.Macro;
28 import ij.gui.GenericDialog;
29 import ij.gui.YesNoCancelDialog;
30 import ij.io.DirectoryChooser;
31 import ini.trakem2.display.*;
32 import ini.trakem2.persistence.DBObject;
33 import ini.trakem2.persistence.DBLoader;
34 import ini.trakem2.persistence.FSLoader;
35 import ini.trakem2.persistence.Loader;
36 import ini.trakem2.tree.DTDParser;
37 import ini.trakem2.tree.DNDTree;
38 import ini.trakem2.tree.LayerThing;
39 import ini.trakem2.tree.LayerTree;
40 import ini.trakem2.tree.ProjectThing;
41 import ini.trakem2.tree.ProjectTree;
42 import ini.trakem2.tree.TemplateThing;
43 import ini.trakem2.tree.TemplateTree;
44 import ini.trakem2.tree.Thing;
45 import ini.trakem2.utils.IJError;
46 import ini.trakem2.utils.ProjectToolbar;
47 import ini.trakem2.utils.Utils;
48 import ini.trakem2.utils.Bureaucrat;
49 import ini.trakem2.vector.Compare;
51 import java.io.File;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Enumeration;
55 import java.util.HashMap;
56 import java.util.Hashtable;
57 import java.util.HashSet;
58 import java.util.Iterator;
59 import java.util.Map;
60 import javax.swing.tree.*;
61 import javax.swing.JTree;
62 import java.awt.Rectangle;
64 /** The top-level class in control. */
65 public class Project extends DBObject {
67 /* // using virtual frame buffer instead, since the trees are needed
68 public static final boolean headless = isHeadless();
70 private static boolean isHeadless() {
71 return Boolean.parseBoolean(System.getProperty("java.awt.headless"));
75 /** Keep track of all open projects. */
76 static private ArrayList<Project> al_open_projects = new ArrayList<Project>();
78 private Loader loader;
80 private TemplateTree template_tree = null;
82 private ProjectTree project_tree = null;
84 /** The root Thing that holds the project. */
85 private ProjectThing root_pt;
87 /** The root LayerThing of the LayerTree. */
88 private LayerThing root_lt;
90 /** The root TemplateThing of the TemplateTree. */
91 private TemplateThing root_tt;
93 /** The root LayerSet that holds the layers. */
94 private LayerSet layer_set;
96 static private TemplateThing layer_template = null;
97 static private TemplateThing layer_set_template = null;
99 /** The hashtable of unique TemplateThing types; the key is the type (String). */
100 private HashMap<String,TemplateThing> ht_unique_tt = null;
102 private LayerTree layer_tree = null;
104 private String title = "Project"; // default // TODO should be an attribute in the ProjectThing that holds it
106 private final HashMap<String,String> ht_props = new HashMap<String,String>();
108 /** Intercept ImageJ menu commands if the front image is a FakeImagePlus. */
109 static private final ImageJCommandListener command_listener = new ImageJCommandListener();
111 /** Universal near-unique id for this project, consisting of:
112 * <creation-time>.<storage-folder-hashcode>.<username-hashcode> */
113 private String unuid = null;
115 /** The constructor used by the static methods present in this class. */
116 private Project(Loader loader) {
117 super(loader);
118 this.loader = loader;
119 this.project = this; // for the superclass DBObject
120 loader.addToDatabase(this);
123 /** Constructor used by the Loader to find projects. These projects contain no loader. */
124 public Project(long id, String title) {
125 super(null, id);
126 this.title = title;
127 this.project = this;
130 static public Project getProject(final String title) {
131 for (final Project pr : al_open_projects) {
132 if (pr.title.equals(title)) return pr;
134 return null;
137 /** Return a copy of the list of all open projects. */
138 static public ArrayList<Project> getProjects() {
139 return (ArrayList<Project>)al_open_projects.clone();
142 /** Create a new PostgreSQL-based TrakEM2 project. */
143 static public Project newDBProject() {
144 if (Utils.wrongImageJVersion()) return null;
145 // create
146 DBLoader loader = new DBLoader();
147 // check connection settings
148 if (!loader.isReady()) return null;
149 // check connection
150 if (!loader.isConnected()) {
151 Utils.showMessage("Can't talk to database.");
152 return null;
154 return createNewProject(loader, true);
157 /** Open a TrakEM2 project from the database. Queries the database for existing projects and if more than one, asks which one to open. */
158 static public Project openDBProject() {
159 if (Utils.wrongImageJVersion()) return null;
160 DBLoader loader = new DBLoader();
161 if (!loader.isReady()) return null;
162 // check connection
163 if (!loader.isConnected()) {
164 Utils.showMessage("Can't talk to database.");
165 loader.destroy();
166 return null;
168 // query the database for existing projects
169 Project[] projects = loader.getProjects();
170 if (null == projects) {
171 Utils.showMessage("Can't talk to database (null list).");
172 loader.destroy();
173 return null;
175 Project project = null;
176 if (null == projects) {
177 Utils.showMessage("Can't fetch list of projects.");
178 loader.destroy();
179 return null;
180 } else if (0 == projects.length) {
181 Utils.showMessage("No projects in this database.");
182 loader.destroy();
183 return null;
184 } else if (1 == projects.length) {
185 project = projects[0];
186 } else {
187 // ask to choose one
188 String[] titles = new String[projects.length];
189 for (int i=0; i<projects.length; i++) {
190 titles[i] = projects[i].title;
192 GenericDialog gd = new GenericDialog("Choose");
193 gd.addMessage("Choose project to open:");
194 gd.addChoice("project: ", titles, titles[titles.length -1]);
195 gd.showDialog();
196 if (gd.wasCanceled()) {
197 loader.destroy();
198 return null;
200 project = projects[gd.getNextChoiceIndex()];
202 // check if the selected project is open already
203 Iterator it = al_open_projects.iterator();
204 while (it.hasNext()) {
205 Project p = (Project)it.next();
206 if (loader.isIdenticalProjectSource(p.loader) && p.id == project.id && p.title.equals(project.title)) {
207 Utils.showMessage("A project with title " + p.title + " and id " + p.id + " from the same database is already open.");
208 loader.destroy();
209 return null;
213 // now, open the selected project
215 // assign loader
216 project.loader = loader;
217 // grab the XML template
218 TemplateThing template_root = loader.getTemplateRoot(project);
219 if (null == template_root) {
220 Utils.showMessage("Failed to retrieve the template tree.");
221 project.destroy();
222 return null;
224 project.template_tree = new TemplateTree(project, template_root);
225 project.ht_unique_tt = template_root.getUniqueTypes(new HashMap<String,TemplateThing>());
226 // create the project Thing, to be root of the whole user Thing tree (and load all its objects)
227 HashMap hs_d = new HashMap(); // to collect all created displayables, and then reassign to the proper layers.
228 try {
229 // create a template for the project Thing
230 TemplateThing project_template = new TemplateThing("project");
231 project.ht_unique_tt.put("project", project_template);
232 project_template.addChild(template_root);
233 project.root_pt = loader.getRootProjectThing(project, template_root, project_template, hs_d);
234 // restore parent/child and attribute ownership and values (now that all Things exist)
235 project.root_pt.setup();
236 } catch (Exception e) {
237 Utils.showMessage("Failed to retrieve the Thing tree for the project.");
238 IJError.print(e);
239 project.destroy();
240 return null;
242 // create the user objects tree
243 project.project_tree = new ProjectTree(project, project.root_pt);
244 // restore the expanded state of each node
245 loader.restoreNodesExpandedState(project);
247 // create the layers templates
248 project.createLayerTemplates();
249 // fetch the root layer thing and the root layer set (will load all layers and layer sets, with minimal contents of patches; gets the basic objects -profile, pipe, etc.- from the project.root_pt). Will open all existing displays for each layer.
250 LayerThing root_layer_thing = null;
251 try {
252 root_layer_thing = loader.getRootLayerThing(project, project.root_pt, project.layer_set_template, project.layer_template);
253 if (null == root_layer_thing) {
254 project.destroy();
255 Utils.showMessage("Could not retrieve the root layer thing.");
256 return null;
258 // set the child/parent relationships now that everything exists
259 root_layer_thing.setup();
260 project.layer_set = (LayerSet)root_layer_thing.getObject();
261 if (null == project.layer_set) {
262 project.destroy();
263 Utils.showMessage("Could not retrieve the root layer set.");
264 return null;
266 project.layer_set.setup(); // set the active layer to each ZDisplayable
268 // debug:
269 //Utils.log2("$$$ root_lt: " + root_layer_thing + " ob: " + root_layer_thing.getObject().getClass().getName() + "\n children: " + ((LayerSet)root_layer_thing.getObject()).getLayers().size());
271 project.layer_tree = new LayerTree(project, root_layer_thing);
272 project.root_lt = root_layer_thing;
273 } catch (Exception e) {
274 Utils.showMessage("Failed to retrieve the Layer tree for the project.");
275 IJError.print(e);
276 project.destroy();
277 return null;
280 // if all when well, register as open:
281 al_open_projects.add(project);
282 // create the project control window, containing the trees in a double JSplitPane
283 ControlWindow.add(project, project.template_tree, project.project_tree, project.layer_tree);
284 // now open the displays that were stored for later, if any:
285 Display.openLater();
287 return project;
291 /** Creates a new project to be based on .xml and image files, not a database. Images are left where they are, keeping the path to them. If the arg equals 'blank', then no template is asked for. */
292 static public Project newFSProject(String arg) {
293 return newFSProject(arg, null);
296 static public Project newFSProject(String arg, TemplateThing template_root) {
297 return newFSProject(arg, null, null);
300 /** Creates a new project to be based on .xml and image files, not a database. Images are left where they are, keeping the path to them. If the arg equals 'blank', then no template is asked for; if template_root is not null that is used; else, a template file is asked for. */
301 static public Project newFSProject(String arg, TemplateThing template_root, String storage_folder) {
302 if (Utils.wrongImageJVersion()) return null;
303 try {
304 String dir_project = storage_folder;
305 if (null == dir_project || !new File(dir_project).isDirectory()) {
306 DirectoryChooser dc = new DirectoryChooser("Select storage folder");
307 dir_project = dc.getDirectory();
308 if (null == dir_project) return null; // user cancelled dialog
309 if (!Loader.canReadAndWriteTo(dir_project)) {
310 Utils.showMessage("Can't read/write to the selected storage folder.\nPlease check folder permissions.");
311 return null;
314 FSLoader loader = new FSLoader(dir_project);
315 if (!loader.isReady()) return null;
316 Project project = createNewProject(loader, !("blank".equals(arg) || "amira".equals(arg)), template_root);
318 // help the helpless users:
319 if (null != project && ControlWindow.isGUIEnabled()) {
320 Utils.log2("Creating automatic Display.");
321 // add a default layer
322 Layer layer = new Layer(project, 0, 1, project.layer_set);
323 project.layer_set.add(layer);
324 project.layer_tree.addLayer(project.layer_set, layer);
325 Display.createDisplay(project, layer);
327 try {
328 Thread.sleep(200); // waiting cheaply for asynchronous swing calls
329 } catch (InterruptedException ie) {
330 ie.printStackTrace();
333 if (arg.equals("amira") || arg.equals("stack")) {
334 // forks into a task thread
335 loader.importStack(project.layer_set.getLayer(0), null, true);
338 return project;
339 } catch (Exception e) {
340 IJError.print(e);
342 return null;
345 static public Project openFSProject(final String path) {
346 return openFSProject(path, true);
349 /** Opens a project from an .xml file. If the path is null it'll be asked for.
350 * Only one project may be opened at a time.
352 synchronized static public Project openFSProject(final String path, final boolean open_displays) {
353 if (Utils.wrongImageJVersion()) return null;
354 final FSLoader loader = new FSLoader();
355 final Object[] data = loader.openFSProject(path, open_displays);
356 if (null == data) {
357 return null;
359 //Macro.setOptions("xml_path=" + loader.getProjectXMLPath()); // TODO gets overwritten by the file dialog, but still, the value is the same. Only the key is different.
360 final TemplateThing root_tt = (TemplateThing)data[0];
361 final ProjectThing root_pt = (ProjectThing)data[1];
362 final LayerThing root_lt = (LayerThing)data[2];
363 final HashMap ht_pt_expanded = (HashMap)data[3];
365 final Project project = (Project)root_pt.getObject();
366 project.createLayerTemplates();
367 project.template_tree = new TemplateTree(project, root_tt);
368 project.root_tt = root_tt;
369 project.root_pt= root_pt;
370 project.project_tree = new ProjectTree(project, project.root_pt);
371 project.layer_tree = new LayerTree(project, root_lt);
372 project.root_lt = root_lt;
373 project.layer_set = (LayerSet)root_lt.getObject();
375 // if all when well, register as open:
376 al_open_projects.add(project);
377 // create the project control window, containing the trees in a double JSplitPane
378 ControlWindow.add(project, project.template_tree, project.project_tree, project.layer_tree);
380 // debug: print the entire root project tree
381 //project.root_pt.debug("");
383 // set ProjectThing nodes expanded state, now that the trees exist
384 try {
385 java.lang.reflect.Field f = JTree.class.getDeclaredField("expandedState");
386 f.setAccessible(true);
387 Hashtable ht_exp = (Hashtable)f.get(project.project_tree);
388 for (Iterator it = ht_pt_expanded.entrySet().iterator(); it.hasNext(); ) {
389 Map.Entry entry = (Map.Entry)it.next();
390 ProjectThing pt = (ProjectThing)entry.getKey();
391 Boolean expanded = (Boolean)entry.getValue();
392 //project.project_tree.expandPath(new TreePath(project.project_tree.findNode(pt, project.project_tree).getPath()));
393 // WARNING the above is wrong in that it will expand the whole thing, not just set the state of the node!!
394 // So the ONLY way to do it is to start from the child-most leafs of the tree, and apply the expanding to them upward. This is RIDICULOUS, how can it be so broken
395 // so, hackerous:
396 DefaultMutableTreeNode nd = project.project_tree.findNode(pt, project.project_tree);
397 //if (null == nd) Utils.log2("null node for " + pt);
398 //else Utils.log2("path: " + new TreePath(nd.getPath()));
399 if (null == nd) {
400 Utils.log2("Can't find node for " + pt);
401 } else {
402 ht_exp.put(new TreePath(nd.getPath()), expanded);
405 project.project_tree.updateUILater(); // very important!!
406 } catch (Exception e) {
407 IJError.print(e);
409 // open any stored displays
410 if (open_displays) {
411 final Bureaucrat burro = Display.openLater();
412 if (null != burro) {
413 final Runnable ru = new Runnable() {
414 public void run() {
415 // wait until the Bureaucrat finishes
416 try { burro.join(); } catch (InterruptedException ie) {}
417 // restore to non-changes (crude, but works)
418 project.loader.setChanged(false);
419 Utils.log2("C set to false");
422 new Thread() {
423 public void run() {
424 setPriority(Thread.NORM_PRIORITY);
425 // avoiding "can't call invokeAndWait from the EventDispatch thread" error
426 try {
427 javax.swing.SwingUtilities.invokeAndWait(ru);
428 } catch (Exception e) {
429 Utils.log2("ERROR: " + e);
432 }.start();
433 // SO: WAIT TILL THE END OF TIME!
434 new Thread() { public void run() {
435 try {
436 Thread.sleep(4000); // ah, the pain in my veins. I can't take this shitty setup anymore.
437 javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() {
438 project.getLoader().setChanged(false);
439 Utils.log2("D set to false");
440 }});
441 project.getTemplateTree().updateUILater(); // repainting to fix gross errors in tree rendering
442 project.getProjectTree().updateUILater(); // idem
443 } catch (Exception ie) {}
444 }}.start();
445 } else {
446 // help the helpless users
447 Display.createDisplay(project, project.layer_set.getLayer(0));
450 return project;
453 static private Project createNewProject(Loader loader, boolean ask_for_template) {
454 return createNewProject(loader, ask_for_template, null);
457 static private Project createNewSubProject(Project source, Loader loader) {
458 return createNewProject(loader, false, source.root_tt, true);
461 static private Project createNewProject(Loader loader, boolean ask_for_template, TemplateThing template_root) {
462 return createNewProject(loader, ask_for_template, template_root, false);
465 static private Project createNewProject(Loader loader, boolean ask_for_template, TemplateThing template_root, boolean clone_ids) {
466 Project project = new Project(loader);
467 // ask for an XML properties file that defines the Thing objects that can be created
468 // (the XML file will be parsed into a TemplateTree filled with TemplateThing objects)
469 //Utils.log2("ask_for_template: " + ask_for_template);
470 if (ask_for_template) template_root = project.loader.askForXMLTemplate(project);
471 if (null == template_root) {
472 template_root = new TemplateThing("anything");
473 } else if (clone_ids) {
474 // the given template_root belongs to another project from which we are cloning
475 template_root = template_root.clone(project, true);
476 } // else, use the given template_root as is.
477 // create tree
478 project.template_tree = new TemplateTree(project, template_root);
479 project.root_tt = template_root;
480 // collect unique TemplateThing instances
481 project.ht_unique_tt = template_root.getUniqueTypes(new HashMap<String,TemplateThing>());
482 // add all TemplateThing objects to the database, recursively
483 if (!clone_ids) template_root.addToDatabase(project);
484 // else already done when cloning the root_tt
486 // create a non-database bound template for the project Thing
487 TemplateThing project_template = new TemplateThing("project");
488 project.ht_unique_tt.put("project", project_template);
489 project_template.addChild(template_root);
490 // create the project Thing, to be root of the whole project thing tree
491 try {
492 project.root_pt= new ProjectThing(project_template, project, project);
493 } catch (Exception e) { IJError.print(e); }
494 // create the user objects tree
495 project.project_tree = new ProjectTree(project, project.root_pt);
496 // create the layer's tree
497 project.createLayerTemplates();
498 project.layer_set = new LayerSet(project, "Top Level", 0, 0, null, 2048, 2048); // initialized with default values, and null parent to signal 'root'
499 try {
500 project.root_lt = new LayerThing(project.layer_set_template, project, project.layer_set);
501 project.layer_tree = new LayerTree(project, project.root_lt);
502 } catch (Exception e) {
503 project.remove();
504 IJError.print(e);
506 // create the project control window, containing the trees in a double JSplitPane
507 ControlWindow.add(project, project.template_tree, project.project_tree, project.layer_tree); // beware that this call is asynchronous, dispatched by the SwingUtilities.invokeLater to avoid havok with Swing components.
508 // register
509 al_open_projects.add(project);
511 return project;
515 public void setTempLoader(Loader loader) {
516 if (null == this.loader) {
517 this.loader = loader;
518 } else {
519 Utils.log2("Project.setTempLoader: already have one.");
523 public final Loader getLoader() {
524 return loader;
527 public String getType() {
528 return "project";
531 public String save() {
532 Thread.yield(); // let it repaint the log window
533 String path = loader.save(this); // TODO: put in a bkgd task, and show a progress bar
534 return path;
537 public String saveAs(String xml_path, boolean overwrite) {
538 return loader.saveAs(xml_path, overwrite);
541 public boolean destroy() {
542 if (loader.hasChanges() && !getBooleanProperty("no_shutdown_hook")) { // DBLoader always returns false
543 if (ControlWindow.isGUIEnabled()) {
544 final YesNoDialog yn = ControlWindow.makeYesNoDialog("TrakEM2", "There are unsaved changes in project " + title + ". Save them?");
545 if (yn.yesPressed()) {
546 loader.save(this);
548 } else {
549 Utils.log2("WARNING: closing project '" + title + "' with unsaved changes.");
552 al_open_projects.remove(this);
553 // flush all memory
554 if (null != loader) { // the last project is destroyed twice for some reason, if several are open. This is a PATCH
555 loader.destroy(); // and disconnect
556 loader = null;
558 ControlWindow.remove(this); // AFTER loader.destroy() call.
559 if (null != template_tree) template_tree.destroy();
560 if (null != project_tree) project_tree.destroy();
561 if (null != layer_tree) layer_tree.destroy();
562 Polyline.flushTraceCache(this);
563 Compare.removeProject(this);
564 this.template_tree = null; // flag to mean: we're closing
565 // close all open Displays
566 Display.close(this);
567 return true;
570 public boolean isBeingDestroyed() {
571 return null == template_tree;
574 /** Remove the project from the database and release memory. */
575 public void remove() {
576 removeFromDatabase();
577 destroy();
580 /** Remove the project from the database and release memory. */
581 public boolean remove(boolean check) {
582 if (!Utils.check("Delete the project " + toString() + " from the database?")) return false;
583 removeFromDatabase();
584 destroy();
585 return true;
588 public void setTitle(String title) {
589 if (null == title) return;
590 this.title = title;
591 ControlWindow.updateTitle(this);
592 loader.updateInDatabase(this, "title");
595 public String toString() {
596 if (null == title || title.equals("Project")) {
597 try {
598 return loader.makeProjectName(); // can't use this.id, because the id system is project-centric and thus all FSLoader projects would have the same id.
599 } catch (Exception e) { Utils.log2("Swing again."); }
601 return title;
604 public String getTitle() {
605 return title;
608 public TemplateTree getTemplateTree() {
609 return template_tree;
612 public LayerTree getLayerTree() {
613 return layer_tree;
616 public ProjectTree getProjectTree() {
617 return project_tree;
620 /** Make an object of the type the TemplateThing can hold. */
621 public Object makeObject(final TemplateThing tt) {
622 final String type = tt.getType();
623 if (type.equals("profile")) {
624 ProjectToolbar.setTool(ProjectToolbar.PENCIL); // this should go elsewhere, in display issues.
625 return new Profile(this, "profile", 0, 0);
626 } else if (type.equals("pipe")) {
627 ProjectToolbar.setTool(ProjectToolbar.PEN);
628 return new Pipe(this, "pipe", 0, 0);
629 } else if (type.equals("polyline")) {
630 ProjectToolbar.setTool(ProjectToolbar.PEN);
631 return new Polyline(this, "polyline");
632 } else if (type.equals("area_list")) {
633 ProjectToolbar.setTool(ProjectToolbar.PEN);
634 return new AreaList(this, "area_list", 0, 0);
635 } else if (type.equals("ball")) {
636 ProjectToolbar.setTool(ProjectToolbar.PEN);
637 return new Ball(this, "ball", 0, 0);
638 } else if (type.equals("dissector")) {
639 ProjectToolbar.setTool(ProjectToolbar.PEN);
640 return new Dissector(this, "dissector", 0, 0);
641 } else if (type.equals("label")) {
642 return new DLabel(this, " ", 0, 0); // never used so far
643 } else {
644 // just the name, for the abstract ones
645 return type;
649 /** Returns true if the type is 'patch', 'layer', 'layer_set', 'profile', 'profile_list' 'pipe'. */
650 static public boolean isBasicType(String type) {
651 if (isProjectType(type)) return true;
652 if (isLayerType(type)) return true;
653 return false;
656 static public boolean isProjectType(String type) {
657 type = type.toLowerCase();
658 if (type.equals("profile_list")) return true;
659 return false;
662 static public boolean isLayerType(String type) {
663 type = type.toLowerCase().replace(' ', '_');
664 if (type.equals("patch")) return true;
665 if (type.equals("area_list")) return true;
666 if (type.equals("label")) return true;
667 if (type.equals("profile")) return true;
668 if (type.equals("pipe")) return true;
669 if (type.equals("polyline")) return true;
670 if (type.equals("ball")) return true;
671 if (type.equals("layer")) return true;
672 if (type.equals("layer set")) return true; // for XML ...
673 return false;
676 /** Remove the ProjectThing that contains the given object, which will remove the object itself as well. */
677 public boolean removeProjectThing(Object object, boolean check) {
678 return removeProjectThing(object, check, false, 0);
681 /** Remove the ProjectThing that contains the given object, which will remove the object itself as well. */
682 public boolean removeProjectThing(Object object, boolean check, boolean remove_empty_parents, int levels) {
683 if (levels < 0) {
684 Utils.log2("Project.removeProjectThing: levels must be zero or above.");
685 return false;
687 // find the Thing
688 DefaultMutableTreeNode root = (DefaultMutableTreeNode)project_tree.getModel().getRoot();
689 Enumeration e = root.depthFirstEnumeration();
690 DefaultMutableTreeNode node = null;
691 while (e.hasMoreElements()) {
692 node = (DefaultMutableTreeNode)e.nextElement();
693 Object ob = node.getUserObject();
694 if (ob instanceof ProjectThing && ((ProjectThing)ob).getObject() == object) {
695 if (check && !Utils.check("Remove " + object.toString() + "?")) return false;
696 // remove the ProjectThing, its object and the node that holds it.
697 project_tree.remove(node, false, remove_empty_parents, levels);
698 return true;
699 } // the above could be done more generic with a Thing.contains(Object), but I want to make sure that the object is contained by a ProjectThing and nothing else.
701 // not found:
702 return false;
706 /** Find the node in the layer tree with a Thing that contains the given object, and set it selected/highlighted, deselecting everything else first. */
707 public void select(final Layer layer) {
708 select(layer, layer_tree);
710 /** Find the node in any tree with a Thing that contains the given Displayable, and set it selected/highlighted, deselecting everything else first. */
711 public void select(final Displayable d) {
712 if (d.getClass() == LayerSet.class) select(d, layer_tree);
713 else select(d, project_tree);
716 private final void select(final Object ob, final JTree tree) {
717 // Find the Thing that contains the object
718 final Thing root_thing = (Thing)((DefaultMutableTreeNode)tree.getModel().getRoot()).getUserObject();
719 final Thing child_thing = root_thing.findChild(ob);
720 // find the node that contains the Thing, and select it
721 DNDTree.selectNode(child_thing, tree);
724 /** Find the ProjectThing instance with the given id. */
725 public ProjectThing find(final long id) {
726 // can't be the Project itself
727 return root_pt.findChild(id);
730 public DBObject findById(final long id) {
731 if (this.id == id) return this;
732 DBObject dbo = layer_set.findById(id);
733 if (null != dbo) return dbo;
734 dbo = root_pt.findChild(id); // could call findObject(id), but all objects must exist in layer sets anyway.
735 if (null != dbo) return dbo;
736 return (DBObject)root_tt.findChild(id);
739 /** Find a LayerThing that contains the given object. */
740 public LayerThing findLayerThing(final Object ob) {
741 final Object lob = root_lt.findChild(ob);
742 return null != lob ? (LayerThing)lob : null;
745 /** Find a ProjectThing that contains the given object. */
746 public ProjectThing findProjectThing(final Object ob) {
747 final Object pob = root_pt.findChild(ob);
748 return null != pob ? (ProjectThing)pob : null;
751 public ProjectThing getRootProjectThing() {
752 return root_pt;
755 public LayerSet getRootLayerSet() {
756 return layer_set;
759 /** Returns the title of the enclosing abstract node in the ProjectTree.*/
760 public String getParentTitle(final Displayable d) {
761 try {
762 ProjectThing thing = (ProjectThing)this.root_pt.findChild(d);
763 ProjectThing parent = (ProjectThing)thing.getParent();
764 if (d instanceof Profile) {
765 parent = (ProjectThing)parent.getParent(); // skip the profile_list
767 if (null == parent) Utils.log2("null parent for " + d);
768 if (null != parent && null == parent.getObject()) {
769 Utils.log2("null ob for parent " + parent + " of " + d);
771 return parent.getObject().toString(); // the abstract thing should be enclosing a String object
772 } catch (Exception e) { IJError.print(e); return null; }
775 /** Searches upstream in the Project tree for things that have a user-defined name, stops at the first and returns it along with all the intermediate ones that only have a type and not a title, appended. */
776 public String getMeaningfulTitle(final Displayable d) {
777 ProjectThing thing = (ProjectThing)this.root_pt.findChild(d);
778 if (null == thing) return d.getTitle(); // happens if there is no associated node
779 String title = new StringBuffer(!thing.getType().equals(d.getTitle()) ? d.getTitle() + " [" : "[").append(thing.getType()).append(' ').append('#').append(d.getId()).append(']').toString();
781 if (!thing.getType().equals(d.getTitle())) {
782 return title;
785 ProjectThing parent = (ProjectThing)thing.getParent();
786 StringBuffer sb = new StringBuffer(title);
787 while (null != parent) {
788 Object ob = parent.getObject();
789 if (ob.getClass() == Project.class) break;
790 String type = parent.getType();
791 if (!ob.equals(type)) { // meaning, something else was typed in as a title
792 sb.insert(0, new StringBuffer(ob.toString()).append(' ').append('[').append(type).append(']').append('/').toString());
793 //title = ob.toString() + " [" + type + "]/" + title;
794 break;
796 sb.insert(0, '/');
797 sb.insert(0, type);
798 //title = type + "/" + title;
799 parent = (ProjectThing)parent.getParent();
801 //return title;
802 return sb.toString();
805 /** Returns the first upstream user-defined name and type, and the id of the displayable tagged at the end.
806 * If no user-defined name is found, then the type is prepended to the id.
808 public String getShortMeaningfulTitle(final Displayable d) {
809 ProjectThing thing = (ProjectThing)this.root_pt.findChild(d);
810 if (null == thing) return d.getTitle(); // happens if there is no associated node
811 return getShortMeaningfulTitle(thing, d);
813 public String getShortMeaningfulTitle(final ProjectThing thing, final Displayable d) {
814 if (thing.getObject() != d) {
815 return thing.toString();
817 ProjectThing parent = (ProjectThing)thing.getParent();
818 String title = "#" + d.getId();
819 while (null != parent) {
820 Object ob = parent.getObject();
821 String type = parent.getType();
822 if (!ob.equals(type)) { // meaning, something else was typed in as a title
823 title = ob.toString() + " [" + type + "] " + title;
824 break;
826 parent = (ProjectThing)parent.getParent();
828 // if nothing found, prepend the type
829 if ('#' == title.charAt(0)) title = Project.getName(d.getClass()) + " " + title;
830 return title;
833 static public String getType(final Class c) {
834 if (AreaList.class == c) return "area_list";
835 if (DLabel.class == c) return "label";
836 return c.getName().toLowerCase();
839 /** Returns the proper TemplateThing for the given type, complete with children and attributes if any. */
840 public TemplateThing getTemplateThing(String type) {
841 return ht_unique_tt.get(type);
844 /** Returns a list of existing unique types in the template tree (thus the 'project' type is not included, nor the label). The basic types are guaranteed to be present even if there are no instances in the template tree. */
845 public String[] getUniqueTypes() {
846 // ensure the basic types (pipe, ball, profile, profile_list) are present
847 if (!ht_unique_tt.containsKey("profile")) ht_unique_tt.put("profile", new TemplateThing("profile"));
848 if (!ht_unique_tt.containsKey("profile_list")) {
849 TemplateThing tpl = new TemplateThing("profile_list");
850 tpl.addChild((TemplateThing) ht_unique_tt.get("profile"));
851 ht_unique_tt.put("profile_list", tpl);
853 if (!ht_unique_tt.containsKey("pipe")) ht_unique_tt.put("pipe", new TemplateThing("pipe"));
854 if (!ht_unique_tt.containsKey("polyline")) ht_unique_tt.put("polyline", new TemplateThing("polyline"));
855 if (!ht_unique_tt.containsKey("ball")) ht_unique_tt.put("ball", new TemplateThing("ball"));
856 if (!ht_unique_tt.containsKey("area_list")) ht_unique_tt.put("area_list", new TemplateThing("area_list"));
857 if (!ht_unique_tt.containsKey("dissector")) ht_unique_tt.put("dissector", new TemplateThing("dissector"));
858 // this should be done automagically by querying the classes in the package ... but java can't do that without peeking into the .jar .class files. Buh.
860 TemplateThing project_tt = ht_unique_tt.remove("project");
861 /* // debug
862 for (Iterator it = ht_unique_tt.keySet().iterator(); it.hasNext(); ) {
863 Utils.log2("class: " + it.next().getClass().getName());
864 } */
865 final String[] ut = new String[ht_unique_tt.size()];
866 ht_unique_tt.keySet().toArray(ut);
867 ht_unique_tt.put("project", project_tt);
868 Arrays.sort(ut);
869 return ut;
872 /** Remove a unique type from the HashMap. Basic types can't be removed. */
873 public boolean removeUniqueType(String type) {
874 if (null == type || isBasicType(type)) return false;
875 return null != ht_unique_tt.remove(type);
878 public boolean typeExists(String type) {
879 return ht_unique_tt.containsKey(type);
882 /** Returns false if the type exists already. */
883 public boolean addUniqueType(TemplateThing tt) {
884 if (null == ht_unique_tt) this.ht_unique_tt = new HashMap();
885 if (ht_unique_tt.containsKey(tt.getType())) return false;
886 ht_unique_tt.put(tt.getType(), tt);
887 return true;
890 public boolean updateTypeName(String old_type, String new_type) {
891 if (ht_unique_tt.containsKey(new_type)) {
892 Utils.showMessage("Can't rename type '" + old_type + "' : a type named '"+new_type+"' already exists!");
893 return false;
895 ht_unique_tt.put(new_type, ht_unique_tt.remove(old_type));
896 return true;
899 private void createLayerTemplates() {
900 if (null == layer_template) {
901 layer_template = new TemplateThing("layer");
902 layer_set_template = new TemplateThing("layer_set");
903 layer_set_template.addChild(layer_template);
904 layer_template.addChild(layer_set_template); // adding a new instance to keep parent/child relationships clean
905 // No need, there won't ever be a loop so far WARNING may change in the future.
909 /** Export the main trakem2 tag wrapping four hierarchies (the project tag, the ProjectTree, and the Top Level LayerSet the latter including all Displayable objects) and a list of displays. */
910 public void exportXML(final java.io.Writer writer, String indent, Object any) throws Exception {
911 StringBuffer sb_body = new StringBuffer();
912 // 1 - opening tag
913 sb_body.append(indent).append("<trakem2>\n");
914 String in = indent + "\t";
915 // 2 - the project itself
916 sb_body.append(in).append("<project \n")
917 .append(in).append("\tid=\"").append(id).append("\"\n")
918 .append(in).append("\ttitle=\"").append(title).append("\"\n");
919 loader.insertXMLOptions(sb_body, in + "\t");
920 for (Iterator it = ht_props.entrySet().iterator(); it.hasNext(); ) {
921 Map.Entry prop = (Map.Entry)it.next();
922 sb_body.append(in).append('\t').append((String)prop.getKey()).append("=\"").append((String)prop.getValue()).append("\"\n");
924 sb_body.append(in).append(">\n");
925 // 3 - export ProjectTree abstract hierachy (skip the root since it wraps the project itself)
926 if (null != root_pt.getChildren()) {
927 String in2 = in + "\t";
928 for (Iterator it = root_pt.getChildren().iterator(); it.hasNext(); ) {
929 ((ProjectThing)it.next()).exportXML(sb_body, in2, any);
932 sb_body.append(in).append("</project>\n");
933 writer.write(sb_body.toString());
934 sb_body.setLength(0);
935 sb_body = null;
936 // 4 - export LayerSet hierarchy of Layer, LayerSet and Displayable objects
937 layer_set.exportXML(writer, in, any);
938 // 5 - export Display objects
939 Display.exportXML(this, writer, in, any);
940 // 6 - closing tag
941 writer.write("</trakem2>\n");
944 /** Export a complete DTD listing to export the project as XML. */
945 public void exportDTD(StringBuffer sb_header, HashSet hs, String indent) {
946 // 1 - TrakEM2 tag that encloses all hierarchies
947 sb_header.append(indent).append("<!ELEMENT ").append("trakem2 (project,t2_layer_set,t2_display)>\n");
948 // 2 - export user-defined templates
949 //TemplateThing root_tt = (TemplateThing)((DefaultMutableTreeNode)((DefaultTreeModel)template_tree.getModel()).getRoot()).getUserObject();
950 sb_header.append(indent).append("<!ELEMENT ").append("project (").append(root_tt.getType()).append(")>\n");
951 sb_header.append(indent).append("<!ATTLIST project id NMTOKEN #REQUIRED>\n");
952 sb_header.append(indent).append("<!ATTLIST project unuid NMTOKEN #REQUIRED>\n");
953 sb_header.append(indent).append("<!ATTLIST project title NMTOKEN #REQUIRED>\n");
954 sb_header.append(indent).append("<!ATTLIST project preprocessor NMTOKEN #REQUIRED>\n");
955 sb_header.append(indent).append("<!ATTLIST project mipmaps_folder NMTOKEN #REQUIRED>\n");
956 sb_header.append(indent).append("<!ATTLIST project storage_folder NMTOKEN #REQUIRED>\n");
957 for (Iterator it = ht_props.entrySet().iterator(); it.hasNext(); ) {
958 Map.Entry prop = (Map.Entry)it.next();
959 sb_header.append(indent).append("<!ATTLIST project ").append((String)prop.getKey()).append(" NMTOKEN #REQUIRED>\n");
961 root_tt.exportDTD(sb_header, hs, indent);
962 // 3 - export all project objects DTD in the Top Level LayerSet
963 Layer.exportDTD(sb_header, hs, indent);
964 LayerSet.exportDTD(sb_header, hs, indent);
965 Ball.exportDTD(sb_header, hs, indent);
966 DLabel.exportDTD(sb_header, hs, indent);
967 Patch.exportDTD(sb_header, hs, indent);
968 Pipe.exportDTD(sb_header, hs, indent);
969 Polyline.exportDTD(sb_header, hs, indent);
970 Profile.exportDTD(sb_header, hs, indent);
971 AreaList.exportDTD(sb_header, hs, indent);
972 Dissector.exportDTD(sb_header, hs, indent);
973 Displayable.exportDTD(sb_header, hs, indent); // the subtypes of all Displayable types
974 // 4 - export Display
975 Display.exportDTD(sb_header, hs, indent);
976 // all the above could be done with reflection, automatically detecting the presence of an exportDTD method.
979 /** Returns the String to be used as Document Type of the XML file, generated from the name of the root template thing.*/
980 public String getDocType() {
981 //TemplateThing root_tt = (TemplateThing)((DefaultMutableTreeNode)((DefaultTreeModel)template_tree.getModel()).getRoot()).getUserObject();
982 return "trakem2_" + root_tt.getType();
985 /** Find an instance containing the given tree. */
986 static public Project getInstance(final DNDTree ob) {
987 for (Iterator it = al_open_projects.iterator(); it.hasNext(); ) {
988 Project project = (Project)it.next();
989 if (project.layer_tree.equals(ob)) return project;
990 if (project.project_tree.equals(ob)) return project;
991 if (project.template_tree.equals(ob)) return project;
993 return null;
996 /** Returns a user-understandable name for the given class. */
997 static public String getName(final Class c) {
998 String name = c.getName();
999 name = name.substring(name.lastIndexOf('.') + 1);
1000 if (name.equals("DLabel")) return "Label";
1001 else if (name.equals("Patch")) return "Image";
1002 //else if (name.equals("Pipe")) return "Tube";
1003 //else if (name.equals("Ball")) return "Sphere group"; // TODO revise consistency with XML templates and so on
1004 else return name;
1007 public String getInfo() {
1008 StringBuffer sb = new StringBuffer("Project id: ");
1009 sb.append(this.id).append("\nProject name: ").append(this.title)
1010 .append("\nTrees:\n")
1011 .append(project_tree.getInfo()).append("\n")
1012 .append(layer_tree.getInfo())
1014 return sb.toString();
1017 static public Project findProject(Loader loader) {
1018 for (Iterator it = al_open_projects.iterator(); it.hasNext(); ) {
1019 Project pro = (Project)it.next();
1020 if (pro.getLoader() == loader) return pro;
1022 return null;
1025 private boolean input_disabled = false;
1027 /** Tells the displays concerning this Project to accept/reject input. */
1028 public void setReceivesInput(boolean b) {
1029 this.input_disabled = !b;
1030 Display.setReceivesInput(this, b);
1033 public boolean isInputEnabled() {
1034 return !input_disabled;
1037 /** Create a new subproject for the given layer range and ROI.
1038 * Create a new Project using the given project as template. This means the DTD of the given project is copied, as well as the storage and mipmaps folders; everything else is empty in the new project. */
1039 public Project createSubproject(final Rectangle roi, final Layer first, final Layer last) {
1040 try {
1041 // The order matters.
1042 final Project pr = new Project(new FSLoader(this.getLoader()));
1043 pr.id = this.id;
1044 // copy properties
1045 pr.title = this.title;
1046 pr.ht_props.putAll(this.ht_props);
1047 // copy template
1048 pr.root_tt = this.root_tt.clone(pr, true);
1049 pr.template_tree = new TemplateTree(pr, pr.root_tt);
1050 pr.ht_unique_tt = root_tt.getUniqueTypes(new HashMap());
1051 TemplateThing project_template = new TemplateThing("project");
1052 project_template.addChild(pr.root_tt);
1053 pr.ht_unique_tt.put("project", project_template);
1054 // create the layers templates
1055 pr.createLayerTemplates();
1056 // copy LayerSet and all involved Displayable objects
1057 pr.layer_set = (LayerSet)this.layer_set.clone(pr, first, last, roi, false, true);
1058 // create layer tree
1059 pr.root_lt = new LayerThing(pr.layer_set_template, pr, pr.layer_set);
1060 pr.layer_tree = new LayerTree(pr, pr.root_lt);
1061 // add layer nodes to the layer tree (solving chicken-and-egg problem)
1062 pr.layer_set.updateLayerTree();
1063 // copy project tree
1064 pr.root_pt = this.root_pt.subclone(pr);
1065 pr.project_tree = new ProjectTree(pr, pr.root_pt);
1066 // not copying node expanded state.
1067 // register
1068 al_open_projects.add(pr);
1069 // add to gui:
1070 ControlWindow.add(pr, pr.template_tree, pr.project_tree, pr.layer_tree);
1072 // Above, the id of each object is preserved from this project into the subproject.
1074 // The abstract structure should be copied in full regardless, without the basic objects
1075 // included if they intersect the roi.
1077 return pr;
1079 } catch (Exception e) { e.printStackTrace(); }
1080 return null;
1083 public void parseXMLOptions(final HashMap ht_attributes) {
1084 ((FSLoader)this.project.getLoader()).parseXMLOptions(ht_attributes);
1085 // all keys that remain are properties
1086 ht_props.putAll(ht_attributes);
1087 for (Iterator it = ht_props.entrySet().iterator(); it.hasNext(); ) {
1088 Map.Entry prop = (Map.Entry)it.next();
1089 Utils.log2("parsed: " + prop.getKey() + "=" + prop.getValue());
1092 public HashMap<String,String> getPropertiesCopy() {
1093 return (HashMap<String,String>)ht_props.clone();
1095 /** Returns null if not defined. */
1096 public String getProperty(final String key) {
1097 return ht_props.get(key);
1099 /** Returns the default value if not defined, or if not a number or not parsable as a number. */
1100 public float getProperty(final String key, final float default_value) {
1101 try {
1102 final String s = ht_props.get(key);
1103 if (null == s) return default_value;
1104 final float num = Float.parseFloat(s);
1105 if (Float.isNaN(num)) return default_value;
1106 return num;
1107 } catch (NumberFormatException nfe) {
1108 IJError.print(nfe);
1110 return default_value;
1112 public boolean getBooleanProperty(final String key) {
1113 return "true".equals(ht_props.get(key));
1115 public void setProperty(final String key, final String value) {
1116 if (null == value) ht_props.remove(key);
1117 else ht_props.put(key, value);
1119 private final boolean addBox(final GenericDialog gd, final Class c) {
1120 final String name = Project.getName(c);
1121 final boolean link = "true".equals(ht_props.get(name.toLowerCase() + "_nolinks"));
1122 gd.addCheckbox(name, link);
1123 return link;
1125 private final void setLinkProp(final boolean before, final boolean after, final Class c) {
1126 if (before) {
1127 if (!after) ht_props.remove(Project.getName(c).toLowerCase()+"_nolinks");
1128 } else if (after) {
1129 ht_props.put(Project.getName(c).toLowerCase()+"_nolinks", "true");
1131 // setting to false would have no meaning, so the link prop is removed
1133 /** Returns true if there were any changes. */
1134 private final boolean adjustProp(final String prop, final boolean before, final boolean after) {
1135 if (before) {
1136 if (!after) ht_props.remove(prop);
1137 } else if (after) {
1138 ht_props.put(prop, "true");
1140 return before != after;
1142 public void adjustProperties() {
1143 // should be more generic, but for now it'll do
1144 GenericDialog gd = new GenericDialog("Properties");
1145 gd.addMessage("Ignore image linking for:");
1146 boolean link_labels = addBox(gd, DLabel.class);
1147 boolean link_arealist = addBox(gd, AreaList.class);
1148 boolean link_pipes = addBox(gd, Pipe.class);
1149 boolean link_polylines = addBox(gd, Polyline.class);
1150 boolean link_balls = addBox(gd, Ball.class);
1151 boolean link_dissectors = addBox(gd, Dissector.class);
1152 boolean dissector_zoom = "true".equals(ht_props.get("dissector_zoom"));
1153 gd.addCheckbox("Zoom-invariant markers for Dissector", dissector_zoom);
1154 boolean no_color_cues = "true".equals(ht_props.get("no_color_cues"));
1155 gd.addCheckbox("Paint_color_cues", !no_color_cues);
1156 gd.addMessage("Currently linked objects\nwill remain so unless\nexplicitly unlinked.");
1157 String current_mode = ht_props.get("image_resizing_mode");
1158 // Forbid area averaging: doesn't work, and it's not faster than gaussian.
1159 if (Utils.indexOf(current_mode, Loader.modes) >= Loader.modes.length) current_mode = Loader.modes[3]; // GAUSSIAN
1160 gd.addChoice("Image_resizing_mode: ", Loader.modes, null == current_mode ? Loader.modes[3] : current_mode);
1161 int current_R = (int)(100 * ini.trakem2.imaging.StitchingTEM.DEFAULT_MIN_R); // make the float a percent
1162 try {
1163 String scR = ht_props.get("min_R");
1164 if (null != scR) current_R = (int)(Double.parseDouble(scR) * 100);
1165 } catch (Exception nfe) {
1166 IJError.print(nfe);
1168 gd.addSlider("min_R: ", 0, 100, current_R);
1170 boolean layer_mipmaps = "true".equals(ht_props.get("layer_mipmaps"));
1171 gd.addCheckbox("Layer_mipmaps", layer_mipmaps);
1172 boolean keep_mipmaps = "true".equals(ht_props.get("keep_mipmaps"));
1173 gd.addCheckbox("Keep_mipmaps_when_deleting_images", keep_mipmaps); // coping with the fact that thee is no Action context ... there should be one in the Worker thread.
1174 int bucket_side = (int)getProperty("bucket_side", Bucket.MIN_BUCKET_SIZE);
1175 gd.addNumericField("Bucket side length: ", bucket_side, 0);
1176 boolean no_shutdown_hook = "true".equals(ht_props.get("no_shutdown_hook"));
1177 gd.addCheckbox("No_shutdown_hook to save the project", no_shutdown_hook);
1179 gd.showDialog();
1181 if (gd.wasCanceled()) return;
1182 setLinkProp(link_labels, gd.getNextBoolean(), DLabel.class);
1183 setLinkProp(link_arealist, gd.getNextBoolean(), AreaList.class);
1184 setLinkProp(link_pipes, gd.getNextBoolean(), Pipe.class);
1185 setLinkProp(link_polylines, gd.getNextBoolean(), Polyline.class);
1186 setLinkProp(link_balls, gd.getNextBoolean(), Ball.class);
1187 setLinkProp(link_dissectors, gd.getNextBoolean(), Dissector.class);
1188 if (adjustProp("dissector_zoom", dissector_zoom, gd.getNextBoolean())) {
1189 Display.repaint(layer_set); // TODO: should repaint nested LayerSets as well
1191 if (adjustProp("no_color_cues", no_color_cues, !gd.getNextBoolean())) {
1192 Display.repaint(layer_set);
1194 setProperty("image_resizing_mode", Loader.modes[gd.getNextChoiceIndex()]);
1195 setProperty("min_R", new Float((float)gd.getNextNumber() / 100).toString());
1196 boolean layer_mipmaps2 = gd.getNextBoolean();
1197 if (adjustProp("layer_mipmaps", layer_mipmaps, layer_mipmaps2)) {
1198 if (layer_mipmaps && !layer_mipmaps2) {
1199 // TODO
1200 // 1 - ask first
1201 // 2 - remove all existing images from layer.mipmaps folder
1202 } else if (!layer_mipmaps && layer_mipmaps2) {
1203 // TODO
1204 // 1 - ask first
1205 // 2 - create de novo all layer mipmaps in a background task
1208 adjustProp("keep_mipmaps", keep_mipmaps, gd.getNextBoolean());
1209 Utils.log2("keep_mipmaps: " + getBooleanProperty("keep_mipmaps"));
1211 bucket_side = (int)gd.getNextNumber();
1212 if (bucket_side > Bucket.MIN_BUCKET_SIZE) {
1213 setProperty("bucket_side", Integer.toString(bucket_side));
1214 layer_set.recreateBuckets(true);
1216 adjustProp("no_shutdown_hook", no_shutdown_hook, gd.getNextBoolean());
1219 /** Return the Universal Near-Unique Id of this project, which may be null for non-FSLoader projects. */
1220 public String getUNUId() {
1221 return loader.getUNUId();