/** TrakEM2 plugin for ImageJ(C). Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt ) This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. You may contact Albert Cardona at acardona at ini.phys.ethz.ch Institute of Neuroinformatics, University of Zurich / ETH, Switzerland. **/ package ini.trakem2; import ij.IJ; import ij.ImageJ; import ij.gui.GenericDialog; import ij.gui.YesNoCancelDialog; import ini.trakem2.display.Display3D; import ini.trakem2.display.ImageJCommandListener; import ini.trakem2.display.YesNoDialog; import ini.trakem2.persistence.Loader; import ini.trakem2.tree.LayerTree; import ini.trakem2.tree.ProjectTree; import ini.trakem2.tree.TemplateTree; import ini.trakem2.utils.IJError; import ini.trakem2.utils.ProjectToolbar; import ini.trakem2.utils.RedPhone; import ini.trakem2.utils.StdOutWindow; import ini.trakem2.utils.Utils; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.awt.image.ImageProducer; import java.lang.reflect.Field; import java.net.URL; import java.util.Enumeration; import java.util.Hashtable; import java.util.Map; import java.util.Set; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; /** Static class that shows one project per tab in a JFrame. * Creates itself when a project requests to be have its trees displayed. * Destroys itself when there are no more projects to show. * * */ public class ControlWindow { static private JFrame frame = null; static private JTabbedPane tabs = null; /** Project instances are keys, JSplitPane are the objects. */ static private Hashtable<Project,JSplitPane> ht_projects = null; /** While the instance is not null, the other fields (frame, tabs, ht_projects) are not null either. */ static private ControlWindow instance = null; /** Control changes to the instance. */ static private final Object LOCK = new Object(); private final RedPhone red_phone = new RedPhone(); static private boolean gui_enabled = true; /** Intercept ImageJ menu commands if the front image is a FakeImagePlus. */ private ImageJCommandListener command_listener; private ControlWindow() { if (null != ij.gui.Toolbar.getInstance()) { ij.gui.Toolbar.getInstance().addMouseListener(tool_listener); } Utils.setup(this); Loader.setupPreloader(this); if (IJ.isWindows() && isGUIEnabled()) StdOutWindow.start(); Display3D.init(); setLookAndFeel(); this.command_listener = new ImageJCommandListener(); this.red_phone.start(); } // private to the package static final ControlWindow getInstance() { synchronized (LOCK) { if (null == instance) instance = new ControlWindow(); return instance; } } static public void setLookAndFeel() { try { if (ij.IJ.isLinux()) { // Nimbus looks great but it's unstable: after a while, swing components stop repainting, throwing all sort of exceptions. //UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel"); UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel"); for (final Frame frame : Frame.getFrames()) { if (frame.isEnabled()) SwingUtilities.updateComponentTreeUI(frame); } // all done above //if (null != frame) SwingUtilities.updateComponentTreeUI(frame); //if (null != IJ.getInstance()) javax.swing.SwingUtilities.updateComponentTreeUI(IJ.getInstance()); //Display.updateComponentTreeUI(); } } catch (ClassNotFoundException cnfe) { Utils.log2("Could not find Nimbus L&F"); } catch (Exception e) { IJError.print(e); } } /** Prevents ControlWindow from displaying projects.*/ static public void setGUIEnabled(boolean b) { gui_enabled = b; if (gui_enabled && null != frame) frame.setVisible(true); } static public final boolean isGUIEnabled() { return gui_enabled; } /** Returns null if there are no projects */ synchronized static public Set<Project> getProjects() { synchronized (LOCK) { if (null == ht_projects) return null; return ht_projects.keySet(); } } static private MouseListener tool_listener = new MouseAdapter() { private int last_tool = ij.gui.Toolbar.RECTANGLE; public void mousePressed(MouseEvent me) { int tool = ini.trakem2.utils.ProjectToolbar.getToolId(); if (tool != last_tool) { last_tool = tool; ini.trakem2.display.Display.toolChanged(tool); } } }; static private void destroy() { synchronized(LOCK) { if (null == instance) return; if (IJ.isWindows()) StdOutWindow.quit(); Display3D.destroy(); if (null != ht_projects) { // destroy open projects, release memory Enumeration<Project> e = ht_projects.keys(); Project[] project = new Project[ht_projects.size()]; //concurrent modifications .. int next = 0; while (e.hasMoreElements()) { project[next++] = e.nextElement(); } for (int i=0; i<next; i++) { ht_projects.remove(project[i]); if (!project[i].destroy()) { return; } } ht_projects = null; } if (null != tabs) { tabs.removeMouseListener((tabs.getMouseListeners())[0]); tabs = null; } if (null != frame) { final JFrame fr = frame; SwingUtilities.invokeLater(new Runnable() { public void run() { fr.setVisible(false); fr.dispose(); if (null != ij.gui.Toolbar.getInstance()) ij.gui.Toolbar.getInstance().repaint(); }}); frame = null; ProjectToolbar.destroy(); } if (null != tool_listener && null != ij.gui.Toolbar.getInstance()) { ij.gui.Toolbar.getInstance().removeMouseListener(tool_listener); } Utils.destroy(instance); Loader.destroyPreloader(instance); instance.command_listener.destroy(); instance.command_listener = null; if (null != instance.red_phone) instance.red_phone.quit(); instance = null; } } static private boolean hooked = false; /** Beware that this method is asynchronous, as it delegates the launching to the SwingUtilities.invokeLater method to avoid havoc with Swing components. */ static public void add(final Project project, final TemplateTree template_tree, final ProjectTree thing_tree, final LayerTree layer_tree) { final Runnable[] other = new Runnable[2]; if (!gui_enabled) { return; } final Runnable gui_thread = new Runnable() { public void run() { synchronized (LOCK) { getInstance(); // init if (null == frame) { if (!hooked) { 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. public void run() { // threaded quit???// if (null != IJ.getInstance() && !IJ.getInstance().quitting()) IJ.getInstance().quit(); // to ensure the Project offers a YesNoDialog, not a YesNoCancelDialog ControlWindow.destroy(); } }); hooked = true; } frame = createJFrame("TrakEM2"); frame.setBackground(Color.white); frame.getContentPane().setBackground(Color.white); frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent we) { synchronized (LOCK) { if (!Utils.check("Close " + (1 == ht_projects.size() ? "the project?" : "all projects?"))) { return; } destroy(); } } public void windowClosed(WindowEvent we) { // ImageJ is quitting (never detected, so I added the dispose extension above) destroy(); } }); tabs = new JTabbedPane(JTabbedPane.TOP); tabs.setBackground(Color.white); tabs.setMinimumSize(new Dimension(500, 400)); tabs.addMouseListener(new TabListener()); frame.getContentPane().add(tabs); // register with ij.WindowManager so that when ImageJ quits it can be detected // ADDS annoying dialog "Are you sure you want to close ImageJ?"//ij.WindowManager.addWindow(frame); // Make the JPopupMenu instances be heavy weight components by default in Windows and elsewhere, not macosx. if (!ij.IJ.isMacOSX()) JPopupMenu.setDefaultLightWeightPopupEnabled(false); // make the tool tip text for JLabel be heavy weight so they don't hide under the AWT DisplayCanvas javax.swing.ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false); } // create the tab final JSplitPane tab = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); tab.setBackground(Color.white); // 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) if (null == ht_projects) ht_projects = new Hashtable<Project,JSplitPane>(); ht_projects.put(project, tab); // create a scrolling pane for the template_tree final JScrollPane scroll_template = new JScrollPane(template_tree); scroll_template.setBackground(Color.white); scroll_template.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5), "Template")); scroll_template.setMinimumSize(new Dimension(0, 100)); scroll_template.setPreferredSize(new Dimension(300, 400)); // create a scrolling pane for the thing_tree final JScrollPane scroll_things = new JScrollPane(thing_tree); scroll_things.setBackground(Color.white); scroll_things.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5), "Project Objects")); scroll_things.setMinimumSize(new Dimension(0, 100)); scroll_things.setPreferredSize(new Dimension(300, 400)); // create a scrolling pane for the layer_tree final JScrollPane scroll_layers = new JScrollPane(layer_tree); scroll_layers.setBackground(Color.white); scroll_layers.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5), "Layers")); scroll_layers.setMinimumSize(new Dimension(0, 100)); scroll_layers.setPreferredSize(new Dimension(300, 400)); // make a new tab for the project final JSplitPane left = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scroll_template, scroll_things); left.setBackground(Color.white); left.setPreferredSize(new Dimension(600, 400)); // setup the tab tab.setBackground(Color.white); tab.setLeftComponent(left); tab.setRightComponent(scroll_layers); tab.setPreferredSize(new Dimension(900, 400)); // add the tab, titled with the project title tabs.addTab(project.toString(), new CloseIcon(), tab); tabs.setSelectedIndex(tabs.getTabCount() -1); // the frame is created ANYWAY, it is just not made visible if !gui_enabled if (!frame.isVisible() && gui_enabled) { frame.pack(); frame.setVisible(true); frame.toFront(); } Rectangle bounds = frame.getBounds(); if (bounds.width < 200) { frame.setSize(new Dimension(200, bounds.height > 100 ? bounds.height : 100)); frame.pack(); } // now set minimum size again, after showing it (stupid Java), so they are shown correctly (opened) but can be completely collapsed to the sides. try { Thread.sleep(100); } catch (Exception e) {} //scroll_template.setMinimumSize(new Dimension(0, 100)); //scroll_things.setMinimumSize(new Dimension(0, 100)); //scroll_layers.setMinimumSize(new Dimension(0, 100)); tab.setDividerLocation(0.66D); // first, so that left is visible! setDividerLocation depends on the dimensions as they are when painted on the screen left.setDividerLocation(0.5D); // select the SELECT tool if it's the first open project if (1 == ht_projects.size() && gui_enabled) { ProjectToolbar.setTool(ProjectToolbar.SELECT); } // so wait until the setDividerLocation of the 'tab' has finished, then do the left one other[0] = new Runnable() { public void run() { tab.setDividerLocation(0.66D); } }; other[1] = new Runnable() { public void run() { left.setDividerLocation(0.5D); } }; // FINALLY! WHAT DEGREE OF IDIOCY POSSESSED SWING DEVELOPERS? } }}; new Thread() { { setPriority(Thread.NORM_PRIORITY); } public void run() { try { SwingUtilities.invokeAndWait(gui_thread); for (int i=0; i<other.length; i++) { SwingUtilities.invokeAndWait(other[i]); } //Utils.log2("done"); } catch (Exception e) { IJError.print(e); } } }.start(); } synchronized static public Project getActive() { synchronized (LOCK) { if (null == tabs || 0 == ht_projects.size()) return null; if (1 == ht_projects.size()) return (Project)ht_projects.keySet().iterator().next(); else { Component c = tabs.getSelectedComponent(); for (final Map.Entry<Project,JSplitPane> e : ht_projects.entrySet()) { if (e.getValue().equals(c)) return e.getKey(); } } return null; } } static public void remove(final Project project) { synchronized (LOCK) { if (null == tabs || null == ht_projects) return; if (null == instance) return; if (ht_projects.containsKey(project)) { int n_tabs = 0; JSplitPane tab = (JSplitPane)ht_projects.get(project); tabs.remove(tab); ht_projects.remove(project); n_tabs = tabs.getTabCount(); // close the ControlWindow if no projects remain open. if (0 == n_tabs) { destroy(); } } } } static public void updateTitle(final Project project) { SwingUtilities.invokeLater(new Runnable() { public void run() { synchronized (LOCK) { if (null == tabs) return; if (ht_projects.containsKey(project)) { if (null == instance) return; JSplitPane tab = (JSplitPane)ht_projects.get(project); int index = tabs.indexOfComponent(tab); if (-1 != index) { tabs.setTitleAt(index, project.toString()); } } } } }); } private static class TabListener extends MouseAdapter { public void mouseReleased(MouseEvent me) { if (me.isConsumed()) return; synchronized (LOCK) { if (null == tabs) return; int i_tab = tabs.getSelectedIndex(); Component comp = tabs.getComponentAt(i_tab); Icon icon = tabs.getIconAt(i_tab); if (icon instanceof CloseIcon) { CloseIcon ci = (CloseIcon)icon; // find the project Project project = null; for (final Map.Entry<Project,JSplitPane> e: ht_projects.entrySet()) { project = e.getKey(); if (e.getValue().equals(comp)) break; } if (ci.contains(me.getX(), me.getY())) { if (null == project) return; // ask for confirmation before closing if (!Utils.check("Close the project " + project.toString() + " ?")) { return; } // proceed to close: if (project.destroy()) { // will call ControlWindow.remove(project) ci.flush(); } } else if (2 == me.getClickCount()) { // pop dialog to rename the project if (null == project) return; project.getProjectTree().rename(project.getRootProjectThing()); } } } } } static private class CloseIcon implements Icon { private Icon icon; private BufferedImage img; private int x = 0; private int y = 0; CloseIcon() { img = frame.getGraphicsConfiguration().createCompatibleImage(20, 16, Transparency.TRANSLUCENT); Graphics2D g = img.createGraphics(); g.setColor(Color.black); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.drawOval(4 + 2, 2, 12, 12); g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g.drawLine(4 + 4, 4, 4 + 11, 12); g.drawLine(4 + 4, 12, 4 + 11, 4); icon = new ImageIcon(img); } public void paintIcon(Component c, Graphics g, int x, int y) { // store coordinates of the last painting event this.x = x; this.y = y; icon.paintIcon(c, g, x, y ); } public boolean contains(int x, int y) { return new Rectangle(this.x, this.y, icon.getIconWidth(), icon.getIconHeight()).contains(x, y); } public int getIconWidth() { return icon.getIconWidth(); } public int getIconHeight() { return icon.getIconHeight(); } public void flush() { if (null != img) { img.flush(); img = null; } } } /** For the generic dialogs to be parented properly. */ static public GenericDialog makeGenericDialog(String title) { Frame f = (null == frame ? IJ.getInstance() : (java.awt.Frame)frame); return new GenericDialog(title, f); } /** For the YesNoCancelDialog dialogs to be parented properly. */ static public YesNoCancelDialog makeYesNoCancelDialog(String title, String msg) { Frame f = (null == frame ? IJ.getInstance() : (java.awt.Frame)frame); return new YesNoCancelDialog(f, title, msg); } /** For the YesNoDialog dialogs to be parented properly. */ static public YesNoDialog makeYesNoDialog(String title, String msg) { Frame f = (null == frame ? IJ.getInstance() : (java.awt.Frame)frame); return new YesNoDialog(f, title, msg); } static public void toFront() { synchronized (instance) { if (null != frame) frame.toFront(); } } /** Appends to the buffer data relative to the viewport of the given tree. */ /* static public void exportTreesXML(final Project project, final StringBuffer sb_data, final String indent, final JTree tree) { // find the JSplitPane of the given tree JScrollPane[] jsp = new JScrollPane[1]; jsp[0] = null; findJSP((Container)ht_projects.get(project), tree, jsp); if (null == jsp[0]) { Utils.log2("Cound not find a JScrollPane for the tree."); return; } // else, we have it tree.exportXML(sb_data, indent, jsp[0]); } */ // /** Recursive. */ /* static private void findJSP(final Container parent, final JTree tree, final JScrollPane[] jsp) { if (null != jsp[0]) return; Component[] comps = parent.getComponents(); for (int i=0; i<comps.length; i++) { if (comps[i] instanceof Container) { findJSP(comps[i], tree, jsp); } else if (comps[i].equals(tree)) { jsp[0] = (JScrollPane)parent; // MUST be break; } } } */ static public void startWaitingCursor() { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); } static public void endWaitingCursor() { setCursor(Cursor.getDefaultCursor()); } static private void setCursor(final Cursor c) { Utils.invokeLater(new Runnable() { public void run() { if (null != IJ.getInstance()) IJ.getInstance().setCursor(c); ini.trakem2.display.Display.setCursorToAll(c); if (null != frame && frame.isVisible()) frame.setCursor(c); // the ControlWindow frame }}); } /** Returns -1 if not found. */ synchronized static public int getTabIndex(final Project project) { if (null == project || null == ht_projects) return -1; Component tab = (Component)ht_projects.get(project); if (null == tab) return -1; return tabs.indexOfComponent(tab); } static private Image icon = null; /** Returns a new JFrame with the proper icon from ImageJ.iconPath set, if any. */ static public JFrame createJFrame(final String title) { if (null == instance) return new JFrame(title); return instance.newJFrame(title); } synchronized private JFrame newJFrame(final String title) { final JFrame frame = new JFrame(title); if (null == icon) { try { Field mic = ImageJ.class.getDeclaredField("iconPath"); mic.setAccessible(true); String path = (String) mic.get(IJ.getInstance()); icon = IJ.getInstance().createImage((ImageProducer) new URL("file:" + path).getContent()); } catch (Exception e) {} } if (null != icon) frame.setIconImage(icon); return frame; } }