Nicer tab closing icon for ControlWindow
[trakem2.git] / ini / trakem2 / ControlWindow.java
blobcaaeefa6faf5f0f7ee0b23dd9c021c601d58de0e
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;
25 import ij.IJ;
26 import ij.ImageJ;
27 import ij.gui.GenericDialog;
28 import ij.gui.YesNoCancelDialog;
29 import ini.trakem2.display.YesNoDialog;
30 import ini.trakem2.display.Display3D;
31 import ini.trakem2.display.Display;
32 import ini.trakem2.tree.LayerTree;
33 import ini.trakem2.tree.ProjectTree;
34 import ini.trakem2.tree.TemplateTree;
35 import ini.trakem2.utils.ProjectToolbar;
36 import ini.trakem2.utils.Utils;
37 import ini.trakem2.utils.IJError;
38 import ini.trakem2.utils.StdOutWindow;
39 import ini.trakem2.vector.Compare;
40 import ini.trakem2.persistence.Loader;
42 import javax.swing.*;
43 import java.awt.*;
44 import java.awt.image.BufferedImage;
45 import java.awt.image.ImageProducer;
46 import java.lang.reflect.Field;
47 import java.net.URL;
48 import java.util.Enumeration;
49 import java.util.Hashtable;
50 import java.util.Iterator;
51 import java.util.Set;
52 import java.util.Map;
53 import java.awt.event.*;
56 /** Static class that shows one project per tab in a JFrame.
57 * Creates itself when a project requests to be have its trees displayed.
58 * Destroys itself when there are no more projects to show.
60 * */
61 public class ControlWindow {
63 static private JFrame frame = null;
64 static private JTabbedPane tabs = null;
65 /** Project instances are keys, JSplitPane are the objects. */
66 static private Hashtable ht_projects = null;
67 static private ControlWindow instance = null;
69 static private boolean gui_enabled = true;
71 private ControlWindow() {
72 if (null != ij.gui.Toolbar.getInstance()) {
73 ij.gui.Toolbar.getInstance().addMouseListener(tool_listener);
75 synchronized (this) {
76 Utils.setup(this);
77 Loader.setupPreloader(this);
78 if (IJ.isWindows() && isGUIEnabled()) StdOutWindow.start();
79 Display3D.init();
80 if ("albert".equals(System.getProperty("user.name"))) {
81 try {
82 UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
83 if (null != frame) SwingUtilities.updateComponentTreeUI(frame);
84 Display.updateComponentTreeUI();
85 } catch (ClassNotFoundException cnfe) {
86 Utils.log2("Could not find Nimbus L&F");
87 } catch (Exception e) {
88 IJError.print(e);
94 /** Prevents ControlWindow from displaying projects.*/
95 static public void setGUIEnabled(boolean b) {
96 gui_enabled = b;
97 if (gui_enabled && null != frame) frame.setVisible(true);
100 static public final boolean isGUIEnabled() {
101 return gui_enabled;
104 /** Returns null if there are no projects */
105 synchronized static public Set getProjects() {
106 if (null == ht_projects) return null;
107 return ht_projects.keySet();
110 static private MouseListener tool_listener = new MouseAdapter() {
111 private int last_tool = ij.gui.Toolbar.RECTANGLE;
112 public void mousePressed(MouseEvent me) {
113 int tool = ini.trakem2.utils.ProjectToolbar.getToolId();
114 if (tool != last_tool) {
115 last_tool = tool;
116 ini.trakem2.display.Display.toolChanged(tool);
121 static private void destroy() {
122 if (null == instance) return;
123 synchronized(instance) {
124 if (IJ.isWindows()) StdOutWindow.quit();
125 Compare.destroy();
126 Display3D.destroy();
127 if (null != ht_projects) {
128 // destroy open projects, release memory
129 Enumeration e = ht_projects.keys();
130 Project[] project = new Project[ht_projects.size()]; //concurrent modifications ..
131 int next = 0;
132 while (e.hasMoreElements()) {
133 project[next++] = (Project)e.nextElement();
135 for (int i=0; i<next; i++) {
136 ht_projects.remove(project[i]);
137 if (!project[i].destroy()) {
138 return;
141 ht_projects = null;
143 if (null != tabs) {
144 tabs.removeMouseListener((tabs.getMouseListeners())[0]);
145 tabs = null;
147 if (null != frame) {
148 frame.setVisible(false);
149 frame.dispose();
150 frame = null;
151 ProjectToolbar.destroy();
152 if (null != ij.gui.Toolbar.getInstance()) ij.gui.Toolbar.getInstance().repaint();
153 //ij.WindowManager.removeWindow(frame);
155 if (null != tool_listener && null != ij.gui.Toolbar.getInstance()) {
156 ij.gui.Toolbar.getInstance().removeMouseListener(tool_listener);
158 Utils.destroy(instance);
159 Loader.destroyPreloader(instance);
160 instance = null;
164 static private boolean hooked = false;
166 /** Beware that this method is asynchronous, as it delegates the launching to the SwingUtilities.invokeLater method to avoid havoc with Swing components. */
167 static public void add(final Project project, final TemplateTree template_tree, final ProjectTree thing_tree, final LayerTree layer_tree) {
169 final Runnable[] other = new Runnable[2];
171 final Runnable gui_thread = new Runnable() {
172 public void run() {
175 if (null == instance) {
176 instance = new ControlWindow();
179 synchronized (instance) {
180 if (null == frame) {
181 if (!hooked) {
182 Runtime.getRuntime().addShutdownHook(new Thread() { // necessary to disconnect properly from the database instead of with an EOF, and also to ask to save changes for FSLoader projects.
183 public void run() {
184 // threaded quit???// if (null != IJ.getInstance() && !IJ.getInstance().quitting()) IJ.getInstance().quit(); // to ensure the Project offers a YesNoDialog, not a YesNoCancelDialog
185 ControlWindow.destroy();
188 hooked = true;
190 frame = createJFrame("TrakEM2");
191 frame.setBackground(Color.white);
192 frame.getContentPane().setBackground(Color.white);
193 frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
194 frame.addWindowListener(new WindowAdapter() {
195 public void windowClosing(WindowEvent we) {
196 synchronized (instance) {
197 if (!Utils.check("Close " + (1 == ht_projects.size() ? "the project?" : "all projects?"))) {
198 return;
201 destroy();
203 public void windowClosed(WindowEvent we) {
204 // ImageJ is quitting (never detected, so I added the dispose extension above)
205 destroy();
208 tabs = new JTabbedPane(JTabbedPane.TOP);
209 tabs.setBackground(Color.white);
210 tabs.setMinimumSize(new Dimension(500, 400));
211 tabs.addMouseListener(new TabListener());
212 frame.getContentPane().add(tabs);
213 // register with ij.WindowManager so that when ImageJ quits it can be detected
214 // ADDS annoying dialog "Are you sure you want to close ImageJ?"//ij.WindowManager.addWindow(frame);
215 // Make the JPopupMenu instances be heavy weight components by default in Windows and elsewhere, not macosx.
216 if (!ij.IJ.isMacOSX()) JPopupMenu.setDefaultLightWeightPopupEnabled(false);
217 // make the tool tip text for JLabel be heavy weight so they don't hide under the AWT DisplayCanvas
218 javax.swing.ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false);
221 // create the tab
222 final JSplitPane tab = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
223 tab.setBackground(Color.white);
224 // store the tab linked to the project (before setting the trees, so that they won't get repainted and get in trouble not being able to get a project title if the project has no name)
225 if (null == ht_projects) ht_projects = new Hashtable();
226 ht_projects.put(project, tab);
228 // create a scrolling pane for the template_tree
229 final JScrollPane scroll_template = new JScrollPane(template_tree);
230 scroll_template.setBackground(Color.white);
231 scroll_template.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5), "Template"));
232 scroll_template.setMinimumSize(new Dimension(0, 100));
233 scroll_template.setPreferredSize(new Dimension(300, 400));
235 // create a scrolling pane for the thing_tree
236 final JScrollPane scroll_things = new JScrollPane(thing_tree);
237 scroll_things.setBackground(Color.white);
238 scroll_things.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5), "Project Objects"));
239 scroll_things.setMinimumSize(new Dimension(0, 100));
240 scroll_things.setPreferredSize(new Dimension(300, 400));
242 // create a scrolling pane for the layer_tree
243 final JScrollPane scroll_layers = new JScrollPane(layer_tree);
244 scroll_layers.setBackground(Color.white);
245 scroll_layers.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5), "Layers"));
246 scroll_layers.setMinimumSize(new Dimension(0, 100));
247 scroll_layers.setPreferredSize(new Dimension(300, 400));
249 // make a new tab for the project
250 final JSplitPane left = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scroll_template, scroll_things);
251 left.setBackground(Color.white);
252 left.setPreferredSize(new Dimension(600, 400));
254 // setup the tab
255 tab.setBackground(Color.white);
256 tab.setLeftComponent(left);
257 tab.setRightComponent(scroll_layers);
258 tab.setPreferredSize(new Dimension(900, 400));
260 // add the tab, titled with the project title
261 tabs.addTab(project.toString(), new CloseIcon(), tab);
262 tabs.setSelectedIndex(tabs.getTabCount() -1);
264 // the frame is created ANYWAY, it is just not made visible if !gui_enabled
265 if (!frame.isVisible() && gui_enabled) {
266 frame.pack();
267 frame.setVisible(true);
268 frame.toFront();
270 Rectangle bounds = frame.getBounds();
271 if (bounds.width < 200) {
272 frame.setSize(new Dimension(200, bounds.height > 100 ? bounds.height : 100));
273 frame.pack();
275 // now set minimum size again, after showing it (stupid Java), so they are shown correctly (opened) but can be completely collapsed to the sides.
276 try { Thread.sleep(100); } catch (Exception e) {}
277 //scroll_template.setMinimumSize(new Dimension(0, 100));
278 //scroll_things.setMinimumSize(new Dimension(0, 100));
279 //scroll_layers.setMinimumSize(new Dimension(0, 100));
280 tab.setDividerLocation(0.66D); // first, so that left is visible! setDividerLocation depends on the dimensions as they are when painted on the screen
281 left.setDividerLocation(0.5D);
283 // select the SELECT tool if it's the first open project
284 if (1 == ht_projects.size() && gui_enabled) {
285 ProjectToolbar.setTool(ProjectToolbar.SELECT);
288 // so wait until the setDividerLocation of the 'tab' has finished, then do the left one
289 other[0] = new Runnable() {
290 public void run() {
291 tab.setDividerLocation(0.66D);
294 other[1] = new Runnable() {
295 public void run() {
296 left.setDividerLocation(0.5D);
299 // FINALLY! WHAT DEGREE OF IDIOCY POSSESSED SWING DEVELOPERS?
304 // I hate java: can't call invokeLater from the EventDispatch thread
305 new Thread() {
306 public void run() {
307 try {
308 SwingUtilities.invokeAndWait(gui_thread);
309 for (int i=0; i<other.length; i++) {
310 SwingUtilities.invokeAndWait(other[i]);
312 //Utils.log2("done");
313 } catch (Exception e) { IJError.print(e); }
315 }.start();
318 synchronized static public Project getActive() {
319 if (null == tabs || 0 == ht_projects.size()) return null;
320 if (1 == ht_projects.size()) return (Project)ht_projects.keySet().iterator().next();
321 else {
322 Component c = tabs.getSelectedComponent();
323 for (Iterator it = ht_projects.entrySet().iterator(); it.hasNext(); ) {
324 Map.Entry entry = (Map.Entry)it.next();
325 if (entry.getValue().equals(c)) return (Project)entry.getKey();
328 return null;
331 static public void remove(final Project project) {
332 if (null == tabs || null == ht_projects) return;
333 if (null == instance) return;
334 if (ht_projects.containsKey(project)) {
335 int n_tabs = 0;
336 synchronized (instance) {
337 JSplitPane tab = (JSplitPane)ht_projects.get(project);
338 tabs.remove(tab);
339 ht_projects.remove(project);
340 n_tabs = tabs.getTabCount();
342 // close the ControlWindow if no projects remain open.
343 if (0 == n_tabs) {
344 destroy();
349 static public void updateTitle(final Project project) {
350 if (null == tabs) return;
351 if (ht_projects.containsKey(project)) {
352 SwingUtilities.invokeLater(new Runnable() {
353 public void run() {
354 if (null == instance) return;
355 synchronized (instance) {
356 JSplitPane tab = (JSplitPane)ht_projects.get(project);
357 int index = tabs.indexOfComponent(tab);
358 if (-1 != index) {
359 tabs.setTitleAt(index, project.toString());
367 private static class TabListener extends MouseAdapter {
368 public void mouseReleased(MouseEvent me) {
369 if (me.isConsumed()) return;
370 Icon icon = null;
371 Component comp = null;
372 synchronized (instance) {
373 int i_tab = tabs.getSelectedIndex();
374 comp = tabs.getComponentAt(i_tab);
375 icon = tabs.getIconAt(i_tab);
377 if (icon instanceof CloseIcon) {
378 CloseIcon ci = (CloseIcon)icon;
379 // find the project
380 Project project = null;
381 synchronized (instance) {
382 Enumeration e = ht_projects.keys();
383 while (e.hasMoreElements()) {
384 project = (Project)e.nextElement();
385 if (comp.equals(ht_projects.get(project))) {
386 break;
390 if (ci.contains(me.getX(), me.getY())) {
391 if (null == project) return;
392 // ask for confirmation before closing
393 if (!Utils.check("Close the project " + project.toString() + " ?")) {
394 return;
396 // proceed to close:
397 if (project.destroy()) { // will call ControlWindow.remove(project)
398 ci.flush();
400 } else if (2 == me.getClickCount()) {
401 // pop dialog to rename the project
402 if (null == project) return;
403 project.getProjectTree().rename(project.getRootProjectThing());
409 static private class CloseIcon implements Icon {
411 private Icon icon;
412 private BufferedImage img;
413 private int x = 0;
414 private int y = 0;
416 CloseIcon() {
417 img = frame.getGraphicsConfiguration().createCompatibleImage(20, 16, Transparency.TRANSLUCENT);
418 Graphics2D g = img.createGraphics();
419 g.setColor(Color.black);
420 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
421 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
422 g.drawOval(4 + 2, 2, 12, 12);
423 g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
424 g.drawLine(4 + 4, 4, 4 + 11, 12);
425 g.drawLine(4 + 4, 12, 4 + 11, 4);
426 icon = new ImageIcon(img);
429 public void paintIcon(Component c, Graphics g, int x, int y) {
430 // store coordinates of the last painting event
431 this.x = x;
432 this.y = y;
433 icon.paintIcon(c, g, x, y );
436 public boolean contains(int x, int y) {
437 return new Rectangle(this.x, this.y, icon.getIconWidth(), icon.getIconHeight()).contains(x, y);
440 public int getIconWidth() { return icon.getIconWidth(); }
441 public int getIconHeight() { return icon.getIconHeight(); }
443 public void flush() {
444 if (null != img) {
445 img.flush();
446 img = null;
451 /** For the generic dialogs to be parented properly. */
452 static public GenericDialog makeGenericDialog(String title) {
453 Frame f = (null == frame ? IJ.getInstance() : (java.awt.Frame)frame);
454 return new GenericDialog(title, f);
457 /** For the YesNoCancelDialog dialogs to be parented properly. */
458 static public YesNoCancelDialog makeYesNoCancelDialog(String title, String msg) {
459 Frame f = (null == frame ? IJ.getInstance() : (java.awt.Frame)frame);
460 return new YesNoCancelDialog(f, title, msg);
462 /** For the YesNoDialog dialogs to be parented properly. */
463 static public YesNoDialog makeYesNoDialog(String title, String msg) {
464 Frame f = (null == frame ? IJ.getInstance() : (java.awt.Frame)frame);
465 return new YesNoDialog(f, title, msg);
468 static public void toFront() {
469 synchronized (instance) {
470 if (null != frame) frame.toFront();
474 /** Appends to the buffer data relative to the viewport of the given tree. */
476 static public void exportTreesXML(final Project project, final StringBuffer sb_data, final String indent, final JTree tree) {
477 // find the JSplitPane of the given tree
478 JScrollPane[] jsp = new JScrollPane[1];
479 jsp[0] = null;
480 findJSP((Container)ht_projects.get(project), tree, jsp);
481 if (null == jsp[0]) {
482 Utils.log2("Cound not find a JScrollPane for the tree.");
483 return;
485 // else, we have it
486 tree.exportXML(sb_data, indent, jsp[0]);
490 // /** Recursive. */
492 static private void findJSP(final Container parent, final JTree tree, final JScrollPane[] jsp) {
493 if (null != jsp[0]) return;
494 Component[] comps = parent.getComponents();
495 for (int i=0; i<comps.length; i++) {
496 if (comps[i] instanceof Container) {
497 findJSP(comps[i], tree, jsp);
498 } else if (comps[i].equals(tree)) {
499 jsp[0] = (JScrollPane)parent; // MUST be
500 break;
506 static public void startWaitingCursor() { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); }
508 static public void endWaitingCursor() { setCursor(Cursor.getDefaultCursor()); }
510 static private void setCursor(final Cursor c) {
511 if (null != IJ.getInstance()) IJ.getInstance().setCursor(c);
512 ini.trakem2.display.Display.setCursorToAll(c);
513 if (null != frame && frame.isVisible()) frame.setCursor(c); // the ControlWindow frame
516 /** Returns -1 if not found. */
517 synchronized static public int getTabIndex(final Project project) {
518 if (null == project || null == ht_projects) return -1;
519 Component tab = (Component)ht_projects.get(project);
520 if (null == tab) return -1;
521 return tabs.indexOfComponent(tab);
524 static private Image icon = null;
526 /** Returns a new JFrame with the proper icon from ImageJ.iconPath set, if any. */
527 static public JFrame createJFrame(final String title) {
528 if (null == instance) return new JFrame(title);
529 return instance.newJFrame(title);
531 synchronized private JFrame newJFrame(final String title) {
532 final JFrame frame = new JFrame(title);
534 if (null == icon) {
535 try {
536 Field mic = ImageJ.class.getDeclaredField("iconPath");
537 mic.setAccessible(true);
538 String path = (String) mic.get(IJ.getInstance());
539 icon = IJ.getInstance().createImage((ImageProducer) new URL("file:" + path).getContent());
540 } catch (Exception e) {}
543 if (null != icon) frame.setIconImage(icon);
544 return frame;