001    /*
002     * $Id: UIManager.java,v 1.159 2012/03/14 17:13:02 jbeavers Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    
031    package edu.wisc.ssec.mcidasv.ui;
032    
033    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
034    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.list;
035    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newHashSet;
036    import static edu.wisc.ssec.mcidasv.util.XPathUtils.elements;
037    
038    import java.awt.BorderLayout;
039    import java.awt.Color;
040    import java.awt.Component;
041    import java.awt.Dimension;
042    import java.awt.Font;
043    import java.awt.Graphics;
044    import java.awt.Rectangle;
045    import java.awt.Toolkit;
046    import java.awt.event.ActionEvent;
047    import java.awt.event.ActionListener;
048    import java.awt.event.ComponentEvent;
049    import java.awt.event.ComponentListener;
050    import java.awt.event.MouseAdapter;
051    import java.awt.event.MouseEvent;
052    import java.awt.event.MouseListener;
053    import java.awt.event.WindowEvent;
054    import java.awt.event.WindowListener;
055    import java.lang.reflect.Method;
056    import java.net.URL;
057    import java.util.ArrayList;
058    import java.util.Collection;
059    import java.util.Collections;
060    import java.util.HashMap;
061    import java.util.Hashtable;
062    import java.util.LinkedHashMap;
063    import java.util.LinkedHashSet;
064    import java.util.LinkedList;
065    import java.util.List;
066    import java.util.Map;
067    import java.util.Set;
068    import java.util.Map.Entry;
069    import java.util.concurrent.ConcurrentHashMap;
070    
071    import javax.swing.AbstractAction;
072    import javax.swing.Action;
073    import javax.swing.BorderFactory;
074    import javax.swing.ButtonGroup;
075    import javax.swing.Icon;
076    import javax.swing.ImageIcon;
077    import javax.swing.JButton;
078    import javax.swing.JCheckBox;
079    import javax.swing.JComponent;
080    import javax.swing.JDialog;
081    import javax.swing.JLabel;
082    import javax.swing.JMenu;
083    import javax.swing.JMenuBar;
084    import javax.swing.JMenuItem;
085    import javax.swing.JPanel;
086    import javax.swing.JPopupMenu;
087    import javax.swing.JRadioButtonMenuItem;
088    import javax.swing.JScrollPane;
089    import javax.swing.JTabbedPane;
090    import javax.swing.JTextArea;
091    import javax.swing.JToolBar;
092    import javax.swing.JTree;
093    import javax.swing.KeyStroke;
094    import javax.swing.ScrollPaneConstants;
095    import javax.swing.SwingUtilities;
096    import javax.swing.WindowConstants;
097    import javax.swing.border.BevelBorder;
098    import javax.swing.event.MenuEvent;
099    import javax.swing.event.MenuListener;
100    import javax.swing.event.TreeSelectionEvent;
101    import javax.swing.event.TreeSelectionListener;
102    import javax.swing.tree.DefaultMutableTreeNode;
103    import javax.swing.tree.DefaultTreeCellRenderer;
104    import javax.swing.tree.DefaultTreeModel;
105    import javax.swing.tree.TreeSelectionModel;
106    
107    import org.slf4j.Logger;
108    import org.slf4j.LoggerFactory;
109    import org.w3c.dom.Document;
110    import org.w3c.dom.Element;
111    import org.w3c.dom.NodeList;
112    
113    import ucar.unidata.data.DataChoice;
114    import ucar.unidata.data.DataSelection;
115    import ucar.unidata.data.DataSource;
116    import ucar.unidata.data.DataSourceImpl;
117    import ucar.unidata.idv.ControlDescriptor;
118    import ucar.unidata.idv.IdvPersistenceManager;
119    import ucar.unidata.idv.IdvPreferenceManager;
120    import ucar.unidata.idv.IdvResourceManager;
121    import ucar.unidata.idv.IntegratedDataViewer;
122    import ucar.unidata.idv.SavedBundle;
123    import ucar.unidata.idv.ViewManager;
124    import ucar.unidata.idv.ViewState;
125    import ucar.unidata.idv.IdvResourceManager.XmlIdvResource;
126    import ucar.unidata.idv.control.DisplayControlImpl;
127    import ucar.unidata.idv.ui.DataControlDialog;
128    import ucar.unidata.idv.ui.DataSelectionWidget;
129    import ucar.unidata.idv.ui.DataSelector;
130    import ucar.unidata.idv.ui.IdvComponentGroup;
131    import ucar.unidata.idv.ui.IdvComponentHolder;
132    import ucar.unidata.idv.ui.IdvUIManager;
133    import ucar.unidata.idv.ui.IdvWindow;
134    import ucar.unidata.idv.ui.IdvXmlUi;
135    import ucar.unidata.idv.ui.ViewPanel;
136    import ucar.unidata.idv.ui.WindowInfo;
137    import ucar.unidata.metdata.NamedStationTable;
138    import ucar.unidata.ui.ComponentGroup;
139    import ucar.unidata.ui.ComponentHolder;
140    import ucar.unidata.ui.HttpFormEntry;
141    import ucar.unidata.ui.RovingProgress;
142    import ucar.unidata.ui.XmlUi;
143    import ucar.unidata.util.GuiUtils;
144    import ucar.unidata.util.IOUtil;
145    import ucar.unidata.util.LogUtil;
146    import ucar.unidata.util.Misc;
147    import ucar.unidata.util.Msg;
148    import ucar.unidata.util.ObjectListener;
149    import ucar.unidata.util.StringUtil;
150    import ucar.unidata.util.TwoFacedObject;
151    import ucar.unidata.xml.XmlResourceCollection;
152    import ucar.unidata.xml.XmlUtil;
153    
154    import edu.wisc.ssec.mcidasv.Constants;
155    import edu.wisc.ssec.mcidasv.McIDASV;
156    import edu.wisc.ssec.mcidasv.PersistenceManager;
157    import edu.wisc.ssec.mcidasv.StateManager;
158    import edu.wisc.ssec.mcidasv.supportform.McvStateCollector;
159    import edu.wisc.ssec.mcidasv.supportform.SupportForm;
160    import edu.wisc.ssec.mcidasv.util.Contract;
161    import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
162    import edu.wisc.ssec.mcidasv.util.MemoryMonitor;
163    
164    /**
165     * <p>Derive our own UI manager to do some specific things:
166     * <ul>
167     *   <li>Removing displays</li>
168     *   <li>Showing the dashboard</li>
169     *   <li>Adding toolbar customization options</li>
170     *   <li>Implement the McIDAS-V toolbar as a JToolbar.</li>
171     *   <li>Deal with bundles without component groups.</li>
172     * </ul></p>
173     */
174    // TODO: investigate moving similar unpersisting code to persistence manager.
175    public class UIManager extends IdvUIManager implements ActionListener {
176    
177        private static final Logger logger = LoggerFactory.getLogger(UIManager.class);
178        
179        /** Id of the "New Display Tab" menu item for the file menu */
180        public static final String MENU_NEWDISPLAY_TAB = "file.new.display.tab";
181    
182        /** The tag in the xml ui for creating the special example chooser */
183        public static final String TAG_EXAMPLECHOOSER = "examplechooser";
184    
185        /**
186         * Used to keep track of ViewManagers inside a bundle.
187         */
188        public static final HashMap<String, ViewManager> savedViewManagers =
189            new HashMap<String, ViewManager>();
190    
191        /** 
192         * Property name for whether or not the description field of the support
193         * form should perform line wrapping.
194         * */
195        public static final String PROP_WRAP_SUPPORT_DESC = 
196            "mcidasv.supportform.wrap";
197    
198        /** Action command for manipulating the size of the toolbar icons. */
199        private static final String ACT_ICON_TYPE = "action.toolbar.seticonsize";
200    
201        /** Action command for removing all displays */
202        private static final String ACT_REMOVE_DISPLAYS = "action.displays.remove";
203    
204        /** Action command for showing the dashboard */
205        private static final String ACT_SHOW_DASHBOARD = "action.dashboard.show";
206    
207        /** Action command for showing the dashboard */
208        private static final String ACT_SHOW_DATASELECTOR = "action.dataselector.show";
209    
210        /** Action command for showing the dashboard */
211        private static final String ACT_SHOW_DISPLAYCONTROLLER = "action.displaycontroller.show";
212    
213        /** Action command for displaying the toolbar preference tab. */
214        private static final String ACT_SHOW_PREF = "action.toolbar.showprefs";
215    
216        /** Message shown when an unknown action is in the toolbar. */
217        private static final String BAD_ACTION_MSG = "Unknown action (%s) found in your toolbar. McIDAS-V will continue to load, but there will be no button associated with %s.";
218    
219        /** Menu ID for the {@literal "Restore Saved Views"} submenu. */
220        public static final String MENU_NEWVIEWS = "menu.tools.projections.restoresavedviews";
221    
222        /** Label for the "link" to the toolbar customization preference tab. */
223        private static final String LBL_TB_EDITOR = "Customize...";
224    
225        /** Current representation of toolbar actions. */
226        private ToolbarStyle currentToolbarStyle = 
227            getToolbarStylePref(ToolbarStyle.MEDIUM);
228    
229        /** The IDV property that reflects the size of the icons. */
230        private static final String PROP_ICON_SIZE = "mcv.ui.iconsize";
231    
232        /** The URL of the script that processes McIDAS-V support requests. */
233        private static final String SUPPORT_REQ_URL = 
234            "https://www.ssec.wisc.edu/mcidas/misc/mc-v/supportreq/support.php";
235    
236        /** Separator to use between window title components. */
237        protected static final String TITLE_SEPARATOR = " - ";
238    
239        /**
240         * <p>The currently "displayed" actions. Keeping this List allows us to get 
241         * away with only reading the XML files upon starting the application and 
242         * only writing the XML files upon exiting the application. This will avoid
243         * those redrawing delays.</p>
244         */
245        private List<String> cachedButtons;
246    
247        /** Stores all available actions. */
248        private final IdvActions idvActions;
249    
250        /** Map of skin ids to their skin resource index. */
251        private Map<String, Integer> skinIds = readSkinIds();
252    
253        /** An easy way to figure out who is holding a given ViewManager. */
254        private Map<ViewManager, ComponentHolder> viewManagers = 
255            new HashMap<ViewManager, ComponentHolder>();
256    
257        private int componentHolderCount;
258        
259        private int componentGroupCount;
260        
261        /** Cache for the results of {@link #getWindowTitleFromSkin(int)}. */
262        private final Map<Integer, String> skinToTitle = new ConcurrentHashMap<Integer, String>();
263    
264        /** Maps menu IDs to {@link JMenu}s. */
265    //    private Hashtable<String, JMenu> menuIds;
266        private Hashtable<String, JMenuItem> menuIds;
267    
268        /** The splash screen (minus easter egg). */
269        private McvSplash splash;
270    
271        /** 
272         * A list of the toolbars that the IDV is playing with. Used to apply 
273         * changes to *all* the toolbars in the application.
274         */
275        private List<JToolBar> toolbars;
276    
277        /**
278         * Keeping the reference to the toolbar menu mouse listener allows us to
279         * avoid constantly rebuilding the menu. 
280         */
281        private MouseListener toolbarMenu;
282    
283        /** Keep the dashboard around so we don't have to re-create it each time. */
284        protected IdvWindow dashboard;
285    
286        /** False until {@link #initDone()}. */
287        protected boolean initDone = false;
288    
289        /** IDV instantiation--nice to keep around to reduce getIdv() calls. */
290        private IntegratedDataViewer idv;
291    
292        /**
293         * Hands off our IDV instantiation to IdvUiManager.
294         *
295         * @param idv The idv
296         */
297        public UIManager(IntegratedDataViewer idv) {
298            super(idv);
299    
300            this.idv = idv;
301    
302            // cache the appropriate data for the toolbar. it'll make updates 
303            // much snappier
304            idvActions = new IdvActions(getIdv(), IdvResourceManager.RSC_ACTIONS);
305            cachedButtons = readToolbar();
306        }
307    
308        /**
309         * Override the IDV method so that we hide component group button.
310         */
311        @Override public IdvWindow createNewWindow(List viewManagers,
312            boolean notifyCollab, String title, String skinPath, Element skinRoot,
313            boolean show, WindowInfo windowInfo) 
314        {
315    
316            if (windowInfo != null) {
317                logger.trace("creating window: title='{}' bounds: {}", title, windowInfo.getBounds());
318            } else {
319                logger.trace("creating window: title='{}' bounds: (no windowinfo)", title);
320            }
321            
322            if (Constants.DATASELECTOR_NAME.equals(title)) {
323                show = false;
324            }
325            if (skinPath.indexOf("dashboard.xml") >= 0) {
326                show = false;
327            }
328    
329            // used to force any new "display" windows to be the same size as the current window.
330            IdvWindow previousWindow = IdvWindow.getActiveWindow();
331    
332            IdvWindow w = super.createNewWindow(viewManagers, notifyCollab, title, 
333                skinPath, skinRoot, show, windowInfo);
334    
335            String iconPath = idv.getProperty(Constants.PROP_APP_ICON, (String)null);
336            ImageIcon icon = GuiUtils.getImageIcon(iconPath, getClass(), true);
337            w.setIconImage(icon.getImage());
338    
339            // try to catch the dashboard
340            if (Constants.DATASELECTOR_NAME.equals(w.getTitle())) {
341                setDashboard(w);
342            } else if (!w.getComponentGroups().isEmpty()) {
343                // otherwise we need to hide the component group header and explicitly
344                // set the size of the window.
345                ((ComponentHolder)w.getComponentGroups().get(0)).setShowHeader(false);
346                if (previousWindow != null) {
347                    Rectangle r = previousWindow.getBounds();
348                    
349                    w.setBounds(new Rectangle(r.x, r.y, r.width, r.height));
350                }
351            } else {
352                logger.trace("creating window with no component groups");
353            }
354    
355            initDisplayShortcuts(w);
356    
357            RovingProgress progress =
358                (RovingProgress)w.getComponent(IdvUIManager.COMP_PROGRESSBAR);
359    
360            if (progress != null) {
361                progress.start();
362            }
363            return w;
364        }
365    
366        /**
367         * Create the display window described by McIDAS-V's default display skin
368         * 
369         * @return {@link IdvWindow} that was created.
370         */
371        public IdvWindow buildDefaultSkin() {
372            return createNewWindow(new ArrayList(), false);
373        }
374    
375        /**
376         * Create a new IdvWindow for the given viewManager. Put the
377         * contents of the viewManager into the window
378         * 
379         * @return The new window
380         */
381        public IdvWindow buildEmptyWindow() {
382            Element root = null;
383            String path = null;
384            String skinName = null;
385    
386            path = getIdv().getProperty("mcv.ui.emptycompgroup", (String)null);
387            if (path != null) {
388                path = path.trim();
389            }
390    
391            if ((path != null) && (path.length() > 0)) {
392                try {
393                    root = XmlUtil.getRoot(path, getClass());
394                    skinName = getStateManager().getTitle();
395                    String tmp = XmlUtil.getAttribute(root, "name", (String)null);
396                    if (tmp == null) {
397                        tmp = IOUtil.stripExtension(IOUtil.getFileTail(path));
398                    }
399                    skinName = skinName + " - " + tmp;
400                } catch (Exception exc) {
401                    logger.error("error building empty window", exc);
402                }
403            }
404    
405            IdvWindow window = createNewWindow(new ArrayList(), false, skinName, path, root, true, null);
406            window.setVisible(true);
407            return window;
408        }
409    
410        
411        /**
412         * Sets {@link #dashboard} to {@code window}. This method also adds some
413         * listeners to {@code window} so that the state of the dashboard is 
414         * automatically saved.
415         * 
416         * @param window The dashboard. Nothing happens if {@link #dashboard} has 
417         * already been set, or this parameter is {@code null}.
418         */
419        private void setDashboard(final IdvWindow window) {
420            if (window == null || dashboard != null)
421                return;
422    
423            dashboard = window;
424            dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
425    
426            final Component comp = dashboard.getComponent();
427            final ucar.unidata.idv.StateManager state = getIdv().getStateManager();
428    
429            // for some reason the component listener's "componentHidden" method
430            // would not fire the *first* time the dashboard is closed/hidden.
431            // the window listener catches it.
432            dashboard.addWindowListener(new WindowListener() {
433                public void windowClosed(final WindowEvent e) {
434                    Boolean saveViz = (Boolean)state.getPreference(Constants.PREF_SAVE_DASHBOARD_VIZ, Boolean.FALSE);
435                    if (saveViz)
436                        state.putPreference(Constants.PROP_SHOWDASHBOARD, false);
437                }
438    
439                public void windowActivated(final WindowEvent e) { }
440                public void windowClosing(final WindowEvent e) { }
441                public void windowDeactivated(final WindowEvent e) { }
442                public void windowDeiconified(final WindowEvent e) { }
443                public void windowIconified(final WindowEvent e) { }
444                public void windowOpened(final WindowEvent e) { }
445            });
446    
447            dashboard.getComponent().addComponentListener(new ComponentListener() {
448                public void componentMoved(final ComponentEvent e) {
449                    state.putPreference(Constants.PROP_DASHBOARD_BOUNDS, comp.getBounds());
450                }
451    
452                public void componentResized(final ComponentEvent e) {
453                    state.putPreference(Constants.PROP_DASHBOARD_BOUNDS, comp.getBounds());
454                }
455    
456                public void componentShown(final ComponentEvent e) { 
457                    Boolean saveViz = (Boolean)state.getPreference(Constants.PREF_SAVE_DASHBOARD_VIZ, Boolean.FALSE);
458                    if (saveViz)
459                        state.putPreference(Constants.PROP_SHOWDASHBOARD, true);
460                }
461    
462                public void componentHidden(final ComponentEvent e) {
463                    Boolean saveViz = (Boolean)state.getPreference(Constants.PREF_SAVE_DASHBOARD_VIZ, Boolean.FALSE);
464                    if (saveViz)
465                        state.putPreference(Constants.PROP_SHOWDASHBOARD, false);
466                }
467            });
468    
469            Rectangle bounds = (Rectangle)state.getPreferenceOrProperty(Constants.PROP_DASHBOARD_BOUNDS);
470            if (bounds != null)
471                comp.setBounds(bounds);
472        }
473    
474        /**
475         * <p>
476         * Attempts to add all component holders in <code>info</code> to
477         * <code>group</code>. Especially useful when unpersisting a bundle and
478         * attempting to deal with its component groups.
479         * </p>
480         * 
481         * @param info The window we want to process.
482         * @param group Receives the holders in <code>info</code>.
483         * 
484         * @return True if there were component groups in <code>info</code>.
485         */
486        public boolean unpersistComponentGroups(final WindowInfo info,
487            final McvComponentGroup group) {
488            Collection<Object> comps = info.getPersistentComponents().values();
489    
490            if (comps.isEmpty())
491                return false;
492    
493            for (Object comp : comps) {
494                // comp is typically always an IdvComponentGroup, but there are
495                // no guarantees...
496                if (! (comp instanceof IdvComponentGroup)) {
497                    System.err.println("DEBUG: non IdvComponentGroup found in persistent components: "
498                                       + comp.getClass().getName());
499                    continue;
500                }
501    
502                IdvComponentGroup bundleGroup = (IdvComponentGroup)comp;
503    
504                // need to make a copy of this list to avoid a rogue
505                // ConcurrentModificationException
506                // TODO: determine which threads are clobbering each other.
507                List<IdvComponentHolder> holders = 
508                    new ArrayList<IdvComponentHolder>(bundleGroup.getDisplayComponents());
509    
510                for (IdvComponentHolder holder : holders)
511                    group.quietAddComponent(holder);
512    
513                group.redoLayout();
514            }
515            return true;
516        }
517    
518        /**
519         * Override IdvUIManager's loadLookAndFeel so that we can force the IDV to
520         * load the Aqua look and feel if requested from the command line.
521         */
522        @Override public void loadLookAndFeel() {
523            if (McIDASV.useAquaLookAndFeel) {
524                // since we must rely on the IDV to do the actual loading (due to
525                // our UIManager's name conflicting with javax.swing.UIManager's
526                // name), save the user's preference, replace it temporarily and
527                // have the IDV do its thing, then overwrite the temp preference
528                // with the saved preference. Blah!
529                String previousLF = getStore().get(PREF_LOOKANDFEEL, (String)null);
530                getStore().put(PREF_LOOKANDFEEL, "apple.laf.AquaLookAndFeel");
531                super.loadLookAndFeel();
532                getStore().put(PREF_LOOKANDFEEL, previousLF);
533            } else {
534                super.loadLookAndFeel();
535            }
536        }
537    
538        @Override public void handleWindowActivated(final IdvWindow window) {
539            List<ViewManager> viewManagers = window.getViewManagers();
540            ViewManager newActive = null;
541            long lastActivatedTime = -1;
542            
543            for (ViewManager viewManager : viewManagers) {
544                if (viewManager.getContents() == null)
545                    continue;
546                
547                if (!viewManager.getContents().isVisible())
548                    continue;
549                
550                lastActiveFrame = window;
551                
552                if (viewManager.getLastTimeActivated() > lastActivatedTime) {
553                    newActive = viewManager;
554                    lastActivatedTime = viewManager.getLastTimeActivated();
555                }
556            }
557            
558            if (newActive != null)
559                getVMManager().setLastActiveViewManager(newActive);
560        }
561        
562        /**
563         * <p>
564         * Handles the windowing portions of bundle loading: wraps things in
565         * component groups (if needed), merges things into existing windows or
566         * creates new windows, and removes displays and data if asked nicely.
567         * </p>
568         * 
569         * @param windows WindowInfos from the bundle.
570         * @param newViewManagers ViewManagers stored in the bundle.
571         * @param okToMerge Put bundled things into an existing window?
572         * @param fromCollab Did this come from the collab stuff?
573         * @param didRemoveAll Remove all data and displays?
574         * 
575         * @see IdvUIManager#unpersistWindowInfo(List, List, boolean, boolean,
576         *      boolean)
577         */
578        @Override public void unpersistWindowInfo(List windows,
579                List newViewManagers, boolean okToMerge, boolean fromCollab,
580                boolean didRemoveAll) 
581            {
582                if (newViewManagers == null)
583                    newViewManagers = new ArrayList<ViewManager>();
584    
585                // keep track of the "old" state if the user wants to remove things.
586                boolean mergeLayers = ((PersistenceManager)getPersistenceManager()).getMergeBundledLayers();
587                List<IdvComponentHolder> holdersBefore = new ArrayList<IdvComponentHolder>();
588                List<IdvWindow> windowsBefore = new ArrayList<IdvWindow>();
589                if (didRemoveAll) {
590                    holdersBefore.addAll(McVGuiUtils.getAllComponentHolders());
591                    windowsBefore.addAll(McVGuiUtils.getAllDisplayWindows());
592                }
593    
594                for (WindowInfo info : (List<WindowInfo>)windows) {
595                    newViewManagers.removeAll(info.getViewManagers());
596                    makeBundledDisplays(info, okToMerge, mergeLayers, fromCollab);
597    
598                    if (mergeLayers)
599                        holdersBefore.addAll(McVGuiUtils.getComponentHolders(info));
600                }
601    //            System.err.println("holdersBefore="+holdersBefore);
602                // no reason to kill the displays if there aren't any windows in the
603                // bundle!
604                if ((mergeLayers) || (didRemoveAll && !windows.isEmpty()))
605                    killOldDisplays(holdersBefore, windowsBefore, (okToMerge || mergeLayers));
606            }
607    
608        /**
609         * <p>
610         * Removes data and displays that existed prior to loading a bundle.
611         * </p>
612         * 
613         * @param oldHolders Component holders around before loading.
614         * @param oldWindows Windows around before loading.
615         * @param merge Were the bundle contents merged into an existing window?
616         */
617        public void killOldDisplays(final List<IdvComponentHolder> oldHolders,
618            final List<IdvWindow> oldWindows, final boolean merge) 
619        {
620    //        System.err.println("killOldDisplays: merge="+merge);
621            // if we merged, this will ensure that any old holders in the merged
622            // window also get removed.
623            if (merge)
624                for (IdvComponentHolder holder : oldHolders)
625                    holder.doRemove();
626    
627            // mop up any windows that no longer have component holders.
628            for (IdvWindow window : oldWindows) {
629                IdvComponentGroup group = McVGuiUtils.getComponentGroup(window);
630    
631                List<IdvComponentHolder> holders =
632                    McVGuiUtils.getComponentHolders(group);
633    
634                // if the old set of holders contains all of this window's
635                // holders, this window can be deleted:
636                // 
637                // this works fine for merging because the okToMerge stuff will
638                // remove all old holders from the current window, but if the
639                // bundle was merged into this window, containsAll() will fail
640                // due to there being a new holder.
641                // 
642                // if the bundle was loaded into its own window, then
643                // all the old windows will pass this test.
644                if (oldHolders.containsAll(holders)) {
645                    group.doRemove();
646                    window.dispose();
647                }
648            }
649        }
650        
651    
652        /**
653         * A hack because Unidata moved the skins (taken from 
654         * {@link IdvPersistenceManager}).
655         * 
656         * @param skinPath original path
657         * @return fixed path
658         */
659        private String fixSkinPath(String skinPath) {
660            if (skinPath == null) {
661                return null;
662            }
663            if (StringUtil.stringMatch(
664                    skinPath, "^/ucar/unidata/idv/resources/[^/]+\\.xml")) {
665                skinPath =
666                    StringUtil.replace(skinPath, "/ucar/unidata/idv/resources/",
667                                       "/ucar/unidata/idv/resources/skins/");
668            }
669            return skinPath;
670        }
671        
672        /**
673         * <p>
674         * Uses the contents of {@code info} to rebuild a display that has been 
675         * bundled. If {@code merge} is true, the displayable parts of the bundle 
676         * will be put into the current window. Otherwise a new window is created 
677         * and the relevant parts of the bundle will occupy that new window.
678         * </p>
679         * 
680         * @param info WindowInfo to use with creating the new window.
681         * @param merge Merge created things into an existing window?
682         */
683        public void makeBundledDisplays(final WindowInfo info, final boolean merge, final boolean mergeLayers, final boolean fromCollab) {
684            // need a way to get the last active view manager (for real)
685            IdvWindow window = IdvWindow.getActiveWindow();
686            ViewManager last = ((PersistenceManager)getPersistenceManager()).getLastViewManager();
687            String skinPath = info.getSkinPath();
688    
689            // create a new window if we're not merging (or the active window is 
690            // invalid), otherwise sticking with the active window is fine.
691            if ((merge || (mergeLayers)) && last != null) {
692                List<IdvWindow> windows = IdvWindow.getWindows();
693                for (IdvWindow tmpWindow : windows) {
694                    if (tmpWindow.getComponentGroups().isEmpty())
695                        continue;
696    
697                    List<IdvComponentGroup> groups = tmpWindow.getComponentGroups();
698                    for (IdvComponentGroup group : groups) {
699                        List<IdvComponentHolder> holders = group.getDisplayComponents();
700                        for (IdvComponentHolder holder : holders) {
701                            List<ViewManager> vms = holder.getViewManagers();
702                            if (vms != null && vms.contains(last)) {
703                                window = tmpWindow;
704    
705                                if (mergeLayers) {
706                                    mergeLayers(info, window, fromCollab);
707                                }
708                                break;
709                            }
710                        }
711                    }
712                }
713            }
714            else if ((window == null) || (!merge) || (window.getComponentGroups().isEmpty())) {
715                try {
716                    Element skinRoot =
717                        XmlUtil.getRoot(Constants.BLANK_COMP_GROUP, getClass());
718    
719                    window = createNewWindow(null, false, "McIDAS-V",
720                        Constants.BLANK_COMP_GROUP, skinRoot, false, null);
721    
722                    window.setBounds(info.getBounds());
723                    window.setVisible(true);
724    
725                } catch (Throwable e) {
726                    e.printStackTrace();
727                }
728            }
729    
730            McvComponentGroup group =
731                (McvComponentGroup)window.getComponentGroups().get(0);
732    
733            // if the bundle contains only component groups, ensure they get merged
734            // into group.
735            unpersistComponentGroups(info, group);
736        }
737    
738        private void mergeLayers(final WindowInfo info, final IdvWindow window, final boolean fromCollab) {
739            List<ViewManager> newVms = McVGuiUtils.getViewManagers(info);
740            List<ViewManager> oldVms = McVGuiUtils.getViewManagers(window);
741    
742            if (oldVms.size() == newVms.size()) {
743                List<ViewManager> merged = new ArrayList<ViewManager>();
744                for (int vmIdx = 0;
745                         (vmIdx < newVms.size())
746                         && (vmIdx < oldVms.size());
747                         vmIdx++) 
748                {
749                    ViewManager newVm = newVms.get(vmIdx);
750                    ViewManager oldVm = oldVms.get(vmIdx);
751                    if (oldVm.canBe(newVm)) {
752                        oldVm.initWith(newVm, fromCollab);
753                        merged.add(newVm);
754                    }
755                }
756                
757                Collection<Object> comps = info.getPersistentComponents().values();
758    
759                for (Object comp : comps) {
760                    if (!(comp instanceof IdvComponentGroup))
761                        continue;
762                    
763                    IdvComponentGroup group = (IdvComponentGroup)comp;
764                    List<IdvComponentHolder> holders = group.getDisplayComponents();
765                    List<IdvComponentHolder> emptyHolders = new ArrayList<IdvComponentHolder>();
766                    for (IdvComponentHolder holder : holders) {
767                        List<ViewManager> vms = holder.getViewManagers();
768                        for (ViewManager vm : merged) {
769                            if (vms.contains(vm)) {
770                                vms.remove(vm);
771                                getVMManager().removeViewManager(vm);
772                                List<DisplayControlImpl> controls = vm.getControlsForLegend();
773                                for (DisplayControlImpl dc : controls) {
774                                    try {
775                                        dc.doRemove();
776                                    } catch (Exception e) { }
777                                    getViewPanel().removeDisplayControl(dc);
778                                    getViewPanel().viewManagerDestroyed(vm);
779                                    
780                                    vm.clearDisplays();
781    
782                                }
783                            }
784                        }
785                        holder.setViewManagers(vms);
786    
787                        if (vms.isEmpty()) {
788                            emptyHolders.add(holder);
789                        }
790                    }
791                    
792                    for (IdvComponentHolder holder : emptyHolders) {
793                        holder.doRemove();
794                        group.removeComponent(holder);
795                    }
796                }
797            }
798        }
799    
800        /**
801         * Make a window title. The format for window titles is:
802         * {@literal <window>TITLE_SEPARATOR<document>}
803         * 
804         * @param win Window title.
805         * @param doc Document or window sub-content.
806         * @return Formatted window title.
807         */
808        protected static String makeTitle(final String win, final String doc) {
809            if (win == null)
810                return "";
811            else if (doc == null)
812                return win;
813            else if (doc.equals("untitled"))
814                return win;
815    
816            return win.concat(TITLE_SEPARATOR).concat(doc);
817        }
818    
819        /**
820         * Make a window title. The format for window titles is:
821         * 
822         * <pre>
823         * &lt;window&gt;TITLE_SEPARATOR&lt;document&gt;TITLE_SEPARATOR&lt;other&gt;
824         * </pre>
825         * 
826         * @param window Window title.
827         * @param document Document or window sub content.
828         * @param other Other content to include.
829         * @return Formatted window title.
830         */
831        protected static String makeTitle(final String window,
832            final String document, final String other) 
833        {
834            if (other == null)
835                return makeTitle(window, document);
836    
837            return window.concat(TITLE_SEPARATOR).concat(document).concat(
838                TITLE_SEPARATOR).concat(other);
839        }
840    
841        /**
842         * Split window title using <code>TITLE_SEPARATOR</code>.
843         * 
844         * @param title The window title to split
845         * @return Parts of the title with the white space trimmed.
846         */
847        protected static String[] splitTitle(final String title) {
848            String[] splt = title.split(TITLE_SEPARATOR);
849            for (int i = 0; i < splt.length; i++) {
850                splt[i] = splt[i].trim();
851            }
852            return splt;
853        }
854    
855        /**
856         * Overridden to prevent the IDV's {@code StateManager} instantiation of {@link ucar.unidata.idv.mac.MacBridge}.
857         * McIDAS-V uses different approaches for OS X compatibility.
858         *
859         * @return Always returns {@code false}.
860         *
861         * @deprecated Use {@link edu.wisc.ssec.mcidasv.McIDASV#isMac()} instead.
862         */
863        // TODO: be sure to bring back the override annotation once we've upgraded our idv.jar.
864        public boolean isMac() {
865            return false;
866        }
867    
868        /* (non-Javadoc)
869         * @see ucar.unidata.idv.ui.IdvUIManager#about()
870         */
871        public void about() {
872            java.awt.EventQueue.invokeLater(new Runnable() {
873                public void run() {
874                    new AboutFrame((McIDASV)idv).setVisible(true);
875                }
876            });
877        }
878    
879        /**
880         * Handles all the ActionEvents that occur for widgets contained within
881         * this class. It's not so pretty, but it isolates the event handling in
882         * one place (and reduces the number of action listeners to one).
883         * 
884         * @param e The event that triggered the call to this method.
885         */
886        public void actionPerformed(ActionEvent e) {
887            String cmd = (String)e.getActionCommand();
888            boolean toolbarEditEvent = false;
889    
890            // handle selecting large icons
891            if (cmd.startsWith(ToolbarStyle.LARGE.getAction())) {
892                currentToolbarStyle = ToolbarStyle.LARGE;
893                toolbarEditEvent = true;
894            }
895    
896            // handle selecting medium icons
897            else if (cmd.startsWith(ToolbarStyle.MEDIUM.getAction())) {
898                currentToolbarStyle = ToolbarStyle.MEDIUM;
899                toolbarEditEvent = true;
900            }
901    
902            // handle selecting small icons
903            else if (cmd.startsWith(ToolbarStyle.SMALL.getAction())) {
904                currentToolbarStyle = ToolbarStyle.SMALL;
905                toolbarEditEvent = true;
906            }
907    
908            // handle the user selecting the show toolbar preference menu item
909            else if (cmd.startsWith(ACT_SHOW_PREF)) {
910                IdvPreferenceManager prefs = idv.getPreferenceManager();
911                prefs.showTab(Constants.PREF_LIST_TOOLBAR);
912                toolbarEditEvent = true;
913            }
914    
915            // handle the user toggling the size of the icon
916            else if (cmd.startsWith(ACT_ICON_TYPE))
917                toolbarEditEvent = true;
918    
919            // handle the user removing displays
920            else if (cmd.startsWith(ACT_REMOVE_DISPLAYS))
921                idv.removeAllDisplays();
922    
923            // handle popping up the dashboard.
924            else if (cmd.startsWith(ACT_SHOW_DASHBOARD))
925                showDashboard();
926    
927            // handle popping up the data explorer.
928            else if (cmd.startsWith(ACT_SHOW_DATASELECTOR))
929                showDashboard("Data Sources");
930    
931            // handle popping up the display controller.
932            else if (cmd.startsWith(ACT_SHOW_DISPLAYCONTROLLER))
933                showDashboard("Layer Controls");
934    
935            else
936                System.err.println("Unsupported action event!");
937    
938            // if the user did something to change the toolbar, hide the current
939            // toolbar, replace it, and then make the new toolbar visible.
940            if (toolbarEditEvent == true) {
941    
942                getStateManager().writePreference(PROP_ICON_SIZE, 
943                    currentToolbarStyle.getSizeAsString());
944    
945                // destroy the menu so it can be properly updated during rebuild
946                toolbarMenu = null;
947    
948                for (JToolBar toolbar : toolbars) {
949                    toolbar.setVisible(false);
950                    populateToolbar(toolbar);
951                    toolbar.setVisible(true);
952                }
953            }
954        }
955    
956        public JComponent getDisplaySelectorComponent() {
957            DefaultMutableTreeNode root = new DefaultMutableTreeNode("");
958            DefaultTreeModel model = new DefaultTreeModel(root);
959            final JTree tree = new JTree(model);
960            tree.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
961            tree.getSelectionModel().setSelectionMode(
962                TreeSelectionModel.SINGLE_TREE_SELECTION
963            );
964            DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
965            renderer.setIcon(null);
966            renderer.setOpenIcon(null);
967            renderer.setClosedIcon(null);
968            tree.setCellRenderer(renderer);
969    
970            for (IdvWindow w : McVGuiUtils.getAllDisplayWindows()) {
971                String title = w.getTitle();
972                TwoFacedObject winTFO = new TwoFacedObject(title, w);
973                DefaultMutableTreeNode winNode = new DefaultMutableTreeNode(winTFO);
974                for (IdvComponentHolder h : McVGuiUtils.getComponentHolders(w)) {
975                    String hName = h.getName();
976                    TwoFacedObject tmp = new TwoFacedObject(hName, h);
977                    DefaultMutableTreeNode holderNode = new DefaultMutableTreeNode(tmp);
978                    //for (ViewManager v : (List<ViewManager>)h.getViewManagers()) {
979                    for (int i = 0; i < h.getViewManagers().size(); i++) {
980                        ViewManager v = (ViewManager)h.getViewManagers().get(i);
981                        String vName = v.getName();
982                        TwoFacedObject tfo = null;
983                        
984                        if (vName != null && vName.length() > 0)
985                            tfo = new TwoFacedObject(vName, v);
986                        else
987                            tfo = new TwoFacedObject(Constants.PANEL_NAME + " " + (i+1), v);
988                        
989                        holderNode.add(new DefaultMutableTreeNode(tfo));
990                    }
991                    winNode.add(holderNode);
992                }
993                root.add(winNode);
994            }
995    
996            // select the appropriate view
997            tree.addTreeSelectionListener(new TreeSelectionListener() {
998                public void valueChanged(TreeSelectionEvent evt) {
999                    DefaultMutableTreeNode node = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();
1000                    if (node == null || !(node.getUserObject() instanceof TwoFacedObject)) {
1001                        return;
1002                    }
1003                    TwoFacedObject tfo = (TwoFacedObject) node.getUserObject();
1004    
1005                    Object obj = tfo.getId();
1006                    if (obj instanceof ViewManager) {
1007                        ViewManager viewManager = (ViewManager) tfo.getId();
1008                        idv.getVMManager().setLastActiveViewManager(viewManager);
1009                    } else if (obj instanceof McvComponentHolder) {
1010                        McvComponentHolder holder = (McvComponentHolder)obj;
1011                        holder.setAsActiveTab();
1012                    } else if (obj instanceof IdvWindow) {
1013                        IdvWindow window = (IdvWindow)obj;
1014                        window.toFront();
1015                    }
1016                }
1017            });
1018    
1019            // expand all the nodes
1020            for (int i = 0; i < tree.getRowCount(); i++) {
1021                tree.expandPath(tree.getPathForRow(i));
1022            }
1023    
1024            return tree;
1025        }
1026    
1027        /**
1028         * Builds the JPopupMenu that appears when a user right-clicks in the
1029         * toolbar.
1030         * 
1031         * @return MouseListener that listens for right-clicks in the toolbar.
1032         */
1033        private MouseListener constructToolbarMenu() {
1034            JMenuItem large = ToolbarStyle.LARGE.buildMenuItem(this);
1035            JMenuItem medium = ToolbarStyle.MEDIUM.buildMenuItem(this);
1036            JMenuItem small = ToolbarStyle.SMALL.buildMenuItem(this);
1037    
1038            JMenuItem toolbarPrefs = new JMenuItem(LBL_TB_EDITOR);
1039            toolbarPrefs.setActionCommand(ACT_SHOW_PREF);
1040            toolbarPrefs.addActionListener(this);
1041    
1042            switch (currentToolbarStyle) {
1043                case LARGE:  
1044                    large.setSelected(true); 
1045                    break;
1046    
1047                case MEDIUM: 
1048                    medium.setSelected(true); 
1049                    break;
1050    
1051                case SMALL: 
1052                    small.setSelected(true); 
1053                    break;
1054    
1055                default:
1056                    break;
1057            }
1058    
1059            ButtonGroup group = new ButtonGroup();
1060            group.add(large);
1061            group.add(medium);
1062            group.add(small);
1063    
1064            JPopupMenu popup = new JPopupMenu();
1065            popup.setBorder(new BevelBorder(BevelBorder.RAISED));
1066            popup.add(large);
1067            popup.add(medium);
1068            popup.add(small);
1069            popup.addSeparator();
1070            popup.add(toolbarPrefs);
1071    
1072            return new PopupListener(popup);
1073        }
1074    
1075        /**
1076         * Queries the stored preferences to determine the preferred 
1077         * {@link ToolbarStyle}. If there was no preference, {@code defaultStyle} 
1078         * is used.
1079         * 
1080         * @param defaultStyle {@code ToolbarStyle} to use if there was no value 
1081         * associated with the toolbar style preference.
1082         * 
1083         * @return The preferred {@code ToolbarStyle} or {@code defaultStyle}.
1084         * 
1085         * @throws AssertionError if {@code PROP_ICON_SIZE} had returned an integer
1086         * value that did not correspond to a valid {@code ToolbarStyle}.
1087         */
1088        private ToolbarStyle getToolbarStylePref(final ToolbarStyle defaultStyle) {
1089            assert defaultStyle != null;
1090            String storedStyle = getStateManager().getPreferenceOrProperty(PROP_ICON_SIZE, (String)null);
1091            if (storedStyle == null)
1092                return defaultStyle;
1093    
1094            int intSize = Integer.valueOf(storedStyle);
1095    
1096            // can't switch on intSize using ToolbarStyles as the case...
1097            if (intSize == ToolbarStyle.LARGE.getSize())
1098                return ToolbarStyle.LARGE;
1099            if (intSize == ToolbarStyle.MEDIUM.getSize())
1100                return ToolbarStyle.MEDIUM;
1101            if (intSize == ToolbarStyle.SMALL.getSize())
1102                return ToolbarStyle.SMALL;
1103    
1104            // uh oh
1105            throw new AssertionError("Invalid preferred icon size: " + intSize);
1106        }
1107    
1108        /**
1109         * Given a valid action and icon size, build a JButton for the toolbar.
1110         * 
1111         * @param action The action whose corresponding icon we want.
1112         * 
1113         * @return A JButton for the given action with an appropriate-sized icon.
1114         */
1115        private JButton buildToolbarButton(String action) {
1116            IdvAction a = idvActions.getAction(action);
1117            if (a == null)
1118                return null;
1119    
1120            JButton button = new JButton(idvActions.getStyledIconFor(action, currentToolbarStyle));
1121    
1122            // the IDV will take care of action handling! so nice!
1123            button.addActionListener(idv);
1124            button.setActionCommand(a.getAttribute(ActionAttribute.ACTION));
1125            button.addMouseListener(toolbarMenu);
1126            button.setToolTipText(a.getAttribute(ActionAttribute.DESCRIPTION));
1127    
1128            return button;
1129        }
1130    
1131        @Override public JPanel doMakeStatusBar(final IdvWindow window) {
1132            if (window == null)
1133                return new JPanel();
1134    
1135            JLabel msgLabel = new JLabel("                         ");
1136            LogUtil.addMessageLogger(msgLabel);
1137    
1138            window.setComponent(COMP_MESSAGELABEL, msgLabel);
1139    
1140            IdvXmlUi xmlUI = window.getXmlUI();
1141            if (xmlUI != null)
1142                xmlUI.addComponent(COMP_MESSAGELABEL, msgLabel);
1143    
1144            JLabel waitLabel = new JLabel(IdvWindow.getNormalIcon());
1145            waitLabel.addMouseListener(new ObjectListener(null) {
1146                public void mouseClicked(final MouseEvent e) {
1147                    getIdv().clearWaitCursor();
1148                }
1149            });
1150            window.setComponent(COMP_WAITLABEL, waitLabel);
1151    
1152            RovingProgress progress = doMakeRovingProgressBar();
1153            window.setComponent(COMP_PROGRESSBAR, progress);
1154    
1155    //        Monitoring label = new MemoryPanel();
1156    //        ((McIDASV)getIdv()).getMonitorManager().addListener(label);
1157    //        window.setComponent(Constants.COMP_MONITORPANEL, label);
1158    
1159            boolean isClockShowing = Boolean.getBoolean(getStateManager().getPreferenceOrProperty(PROP_SHOWCLOCK, "true"));
1160            MemoryMonitor mm = new MemoryMonitor(getIdv(), 75, 95, isClockShowing);
1161            mm.setBorder(getStatusBorder());
1162    
1163            // MAKE PRETTY NOW!
1164            progress.setBorder(getStatusBorder());
1165            waitLabel.setBorder(getStatusBorder());
1166            msgLabel.setBorder(getStatusBorder());
1167    //        ((JPanel)label).setBorder(getStatusBorder());
1168    
1169    //        JPanel msgBar = GuiUtils.leftCenter((JPanel)label, msgLabel);
1170            JPanel msgBar = GuiUtils.leftCenter(mm, msgLabel);
1171            JPanel statusBar = GuiUtils.centerRight(msgBar, progress);
1172            statusBar.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
1173            return statusBar;
1174        }
1175    
1176        /**
1177         * Make the roving progress bar
1178         *
1179         * @return Roving progress bar
1180         */
1181        public RovingProgress doMakeRovingProgressBar() {
1182            RovingProgress progress = new RovingProgress(Constants.MCV_BLUE) {
1183                    private Font labelFont;
1184    //            public boolean drawFilledSquare() {
1185    //                return false;
1186    //            }
1187    
1188                    public void paintInner(Graphics g) {
1189                            //Catch if we're not in a wait state
1190                            if ( !IdvWindow.getWaitState() && super.isRunning()) {
1191                                    stop();
1192                                    return;
1193                            }
1194                            if ( !super.isRunning()) {
1195                                    super.paintInner(g);
1196                                    return;
1197                            }
1198                            super.paintInner(g);
1199                    }
1200                    
1201                    public void paintLabel(Graphics g, Rectangle bounds) {
1202                            if (labelFont == null) {
1203                                    labelFont = g.getFont();
1204                                    labelFont = labelFont.deriveFont(Font.BOLD);
1205                            }
1206                            g.setFont(labelFont);
1207                            g.setColor(Color.black);
1208                            if (DataSourceImpl.getOutstandingGetDataCalls() > 0) {
1209                                    g.drawString(" Reading data", 5, bounds.height - 4);
1210                            }
1211                            else if (!idv.getAllDisplaysIntialized()){
1212                                    g.drawString(" Creating layers", 5, bounds.height - 4);
1213                            }
1214    
1215                    }
1216                    
1217                public synchronized void stop() {
1218                    super.stop();
1219                    super.reset();
1220                }
1221    
1222            };
1223            progress.setPreferredSize(new Dimension(130, 10));
1224            return progress;
1225        }
1226        
1227        /**
1228         * <p>
1229         * Overrides the IDV's getToolbarUI so that McV can return its own toolbar
1230         * and not deal with the way the IDV handles toolbars. This method also
1231         * updates the toolbar data member so that other methods can fool around
1232         * with whatever the IDV thinks is a toolbar (without having to rely on the
1233         * IDV window manager code).
1234         * </p>
1235         * 
1236         * <p>
1237         * Not that the IDV code is bad of course--I just can't handle that pause
1238         * while the toolbar is rebuilt!
1239         * </p>
1240         * 
1241         * @return A new toolbar based on the contents of toolbar.xml.
1242         */
1243        @Override public JComponent getToolbarUI() {
1244            if (toolbars == null)
1245                toolbars = new LinkedList<JToolBar>();
1246    
1247            JToolBar toolbar = new JToolBar();
1248            populateToolbar(toolbar);
1249            toolbars.add(toolbar);
1250            return toolbar;
1251        }
1252    
1253        /**
1254         * Return a McV-style toolbar to the IDV.
1255         * 
1256         * @return A fancy-pants toolbar.
1257         */
1258        @Override protected JComponent doMakeToolbar() {
1259            return getToolbarUI();
1260        }
1261    
1262        /**
1263         * Uses the cached XML to create a toolbar. Any updates to the toolbar 
1264         * happen almost instantly using this approach. Do note that if there are
1265         * any components in the given toolbar they will be removed.
1266         * 
1267         * @param toolbar A reference to the toolbar that needs buttons and stuff.
1268         */
1269        private void populateToolbar(JToolBar toolbar) {
1270            // clear out the toolbar's current denizens, if any. just a nicety.
1271            if (toolbar.getComponentCount() > 0)
1272                toolbar.removeAll();
1273    
1274            // ensure that the toolbar popup menu appears
1275            if (toolbarMenu == null)
1276                toolbarMenu = constructToolbarMenu();
1277    
1278            toolbar.addMouseListener(toolbarMenu);
1279    
1280            // add the actions that should appear in the toolbar.
1281            for (String action : cachedButtons) {
1282    
1283                // null actions are considered separators.
1284                if (action == null) {
1285                    toolbar.addSeparator();
1286                }
1287                // otherwise we've got a button to add
1288                else {
1289                    JButton b = buildToolbarButton(action);
1290                    if (b != null) {
1291                        toolbar.add(b);
1292                    } else {
1293                        String err = String.format(BAD_ACTION_MSG, action, action);
1294                        LogUtil.userErrorMessage(err);
1295                    }
1296                }
1297            }
1298    
1299            toolbar.addSeparator();
1300    
1301            BundleTreeNode treeRoot = buildBundleTree();
1302            if (treeRoot != null) {
1303    
1304                // add the favorite bundles to the toolbar (hello Tom Whittaker!)
1305                for (BundleTreeNode tmp : treeRoot.getChildren()) {
1306    
1307                    // if this node doesn't have a bundle, it's considered a parent
1308                    if (tmp.getBundle() == null)
1309                        addBundleTree(toolbar, tmp);
1310                    // otherwise it's just another button to add.
1311                    else
1312                        addBundle(toolbar, tmp);
1313                }
1314            }
1315        }
1316    
1317        /**
1318         * Given a reference to the current toolbar and a bundle tree node, build a
1319         * button representation of the bundle and add it to the toolbar.
1320         * 
1321         * @param toolbar The toolbar to which we add the bundle.
1322         * @param node The node within the bundle tree that contains our bundle.
1323         */
1324        private void addBundle(JToolBar toolbar, BundleTreeNode node) {
1325            final SavedBundle bundle = node.getBundle();
1326    
1327            ImageIcon fileIcon =
1328                GuiUtils.getImageIcon("/auxdata/ui/icons/File.gif");
1329    
1330            JButton button = new JButton(node.getName(), fileIcon);
1331            button.setToolTipText("Click to open favorite: " + node.getName());
1332            button.addActionListener(new ActionListener() {
1333    
1334                public void actionPerformed(ActionEvent e) {
1335                    // running in a separate thread is kinda nice!
1336                    Misc.run(UIManager.this, "processBundle", bundle);
1337                }
1338            });
1339            toolbar.add(button);
1340        }
1341    
1342        /**
1343         * <p>
1344         * Builds two things, given a toolbar and a tree node: a JButton that
1345         * represents a "first-level" parent node and a JPopupMenu that appears
1346         * upon clicking the JButton. The button is then added to the given
1347         * toolbar.
1348         * </p>
1349         * 
1350         * <p>
1351         * "First-level" means the given node is a child of the root node.
1352         * </p>
1353         * 
1354         * @param toolbar The toolbar to which we add the bundle tree.
1355         * @param node The node we want to add! OMG like duh!
1356         */
1357        private void addBundleTree(JToolBar toolbar, BundleTreeNode node) {
1358            ImageIcon catIcon =
1359                GuiUtils.getImageIcon("/auxdata/ui/icons/Folder.gif");
1360    
1361            final JButton button = new JButton(node.getName(), catIcon);
1362            final JPopupMenu popup = new JPopupMenu();
1363    
1364            button.setToolTipText("Show Favorites category: " + node.getName());
1365    
1366            button.addActionListener(new ActionListener() {
1367                public void actionPerformed(ActionEvent e) {
1368                    popup.show(button, 0, button.getHeight());
1369                }
1370            });
1371    
1372            toolbar.add(button);
1373    
1374            // recurse through the child nodes
1375            for (BundleTreeNode kid : node.getChildren())
1376                buildPopupMenu(kid, popup);
1377        }
1378    
1379        /**
1380         * Writes the currently displayed toolbar buttons to the toolbar XML. This
1381         * has mostly been ripped off from ToolbarEditor. :(
1382         */
1383        public void writeToolbar() {
1384            XmlResourceCollection resources =
1385                getResourceManager()
1386                    .getXmlResources(IdvResourceManager.RSC_TOOLBAR);
1387    
1388            String actionPrefix = "action:";
1389    
1390            // ensure that the IDV can read the XML we're generating.
1391            Document doc = resources.getWritableDocument("<panel/>");
1392            Element root = resources.getWritableRoot("<panel/>");
1393            root.setAttribute(XmlUi.ATTR_LAYOUT, XmlUi.LAYOUT_FLOW);
1394            root.setAttribute(XmlUi.ATTR_MARGIN, "4");
1395            root.setAttribute(XmlUi.ATTR_VSPACE, "0");
1396            root.setAttribute(XmlUi.ATTR_HSPACE, "2");
1397            root.setAttribute(XmlUi.inheritName(XmlUi.ATTR_SPACE), "2");
1398            root.setAttribute(XmlUi.inheritName(XmlUi.ATTR_WIDTH), "5");
1399    
1400            // clear out any pesky kids from previous relationships. XML don't need
1401            // no baby-mama drama.
1402            XmlUtil.removeChildren(root);
1403    
1404            // iterate through the actions that have toolbar buttons in use and add
1405            // 'em to the XML.
1406            for (String action : cachedButtons) {
1407                Element e;
1408                if (action != null) {
1409                    e = doc.createElement(XmlUi.TAG_BUTTON);
1410                    e.setAttribute(XmlUi.ATTR_ACTION, (actionPrefix + action));
1411                } else {
1412                    e = doc.createElement(XmlUi.TAG_FILLER);
1413                    e.setAttribute(XmlUi.ATTR_WIDTH, "5");
1414                }
1415                root.appendChild(e);
1416            }
1417    
1418            // write the XML
1419            try {
1420                resources.writeWritable();
1421            } catch (Exception e) {
1422                e.printStackTrace();
1423            }
1424        }
1425    
1426        /**
1427         * Read the contents of the toolbar XML into a List. We're essentially just
1428         * throwing actions into the list.
1429         * 
1430         * @return The actions/buttons that live in the toolbar xml. Note that if 
1431         * an element is {@code null}, this element represents a {@literal "space"}
1432         * that should appear in both the Toolbar and the Toolbar Preferences.
1433         */
1434        public List<String> readToolbar() {
1435            List<String> data = new ArrayList<String>();
1436    
1437            final Element root = getToolbarRoot();
1438            if (root == null)
1439                return null;
1440    
1441            final NodeList elements = XmlUtil.getElements(root);
1442            for (int i = 0; i < elements.getLength(); i++) {
1443                Element child = (Element)elements.item(i);
1444                if (child.getTagName().equals(XmlUi.TAG_BUTTON))
1445                    data.add(
1446                        XmlUtil.getAttribute(child, ATTR_ACTION, (String)null)
1447                            .substring(7));
1448                else
1449                    data.add(null);
1450            }
1451            return data;
1452        }
1453    
1454        /**
1455         * Returns the icon associated with {@code actionId}. Note that associating
1456         * the {@literal "missing icon"} icon with an action is allowable.
1457         * 
1458         * @param actionId Action ID whose associated icon is to be returned.
1459         * @param style Returned icon's size will be the size associated with the
1460         * specified {@code ToolbarStyle}.
1461         * 
1462         * @return Either the icon corresponding to {@code actionId} or the default
1463         * {@literal "missing icon"} icon.
1464         * 
1465         * @throws NullPointerException if {@code actionId} is null.
1466         */
1467        protected Icon getActionIcon(final String actionId, 
1468            final ToolbarStyle style) 
1469        {
1470            if (actionId == null)
1471                throw new NullPointerException("Action ID cannot be null");
1472    
1473            Icon actionIcon = idvActions.getStyledIconFor(actionId, style);
1474            if (actionIcon != null)
1475                return actionIcon;
1476    
1477            String icon = "/edu/wisc/ssec/mcidasv/resources/icons/toolbar/range-bearing%d.png";
1478            URL tmp = getClass().getResource(String.format(icon, style.getSize()));
1479            return new ImageIcon(tmp);
1480        }
1481    
1482        /**
1483         * Returns the known {@link IdvAction}s in the form of {@link IdvActions}.
1484         * 
1485         * @return {@link #idvActions}
1486         */
1487        public IdvActions getCachedActions() {
1488            return idvActions;
1489        }
1490    
1491        /**
1492         * Returns the actions that currently make up the McIDAS-V toolbar.
1493         * 
1494         * @return {@link List} of {@link ActionAttribute#ID}s that make up the
1495         * current toolbar buttons.
1496         */
1497        public List<String> getCachedButtons() {
1498            if (cachedButtons == null)
1499                cachedButtons = readToolbar();
1500            return cachedButtons;
1501        }
1502    
1503        /**
1504         * Make the menu of actions.
1505         * 
1506         * <p>Overridden in McIDAS-V so that we can fool the IDV into working with
1507         * our icons that allow for multiple {@literal "styles"}.
1508         * 
1509         * @param obj Object to call.
1510         * @param method Method to call.
1511         * @param makeCall if {@code true}, call 
1512         * {@link IntegratedDataViewer#handleAction(String)}.
1513         * 
1514         * @return List of {@link JMenu}s that represent our action menus.
1515         */
1516        @Override public List<JMenu> makeActionMenu(final Object obj, 
1517            final String method, final boolean makeCall) 
1518        {
1519            List<JMenu> menu = arrList();
1520            IdvActions actions = getCachedActions();
1521            for (String group : actions.getAllGroups()) {
1522                List<JMenuItem> items = arrList();
1523                for (IdvAction action : actions.getActionsForGroup(group)) {
1524                    String cmd = (makeCall) ? action.getCommand() : action.getId();
1525                    String desc = action.getAttribute(ActionAttribute.DESCRIPTION);
1526    //                items.add(GuiUtils.makeMenuItem(desc, obj, method, cmd));
1527                    items.add(makeMenuItem(desc, obj, method, cmd));
1528                }
1529    //            menu.add(GuiUtils.makeMenu(group, items));
1530                menu.add(makeMenu(group, items));
1531            }
1532            return menu;
1533        }
1534    
1535        /**
1536         * @see GuiUtils#makeMenuItem(String, Object, String, Object)
1537         */
1538        public static JMenuItem makeMenuItem(String label, Object obj, 
1539            String method, Object arg) 
1540        {
1541            return GuiUtils.makeMenuItem(label, obj, method, arg);
1542        }
1543    
1544        /**
1545         * @see GuiUtils#makeMenu(String, List)
1546         */
1547        @SuppressWarnings("unchecked")
1548        public static JMenu makeMenu(String name, List menuItems) {
1549            return GuiUtils.makeMenu(name, menuItems);
1550        }
1551    
1552        /**
1553         * Returns the collection of action identifiers.
1554         * 
1555         * <p>Overridden in McIDAS-V so that we can fool the IDV into working with
1556         * our icons that allow for multiple {@literal "styles"}.
1557         * 
1558         * @returns A {@link List} of {@link String}s that correspond to 
1559         * {@link IdvAction}s.
1560         */
1561        @Override public List<String> getActions() {
1562            return idvActions.getAttributes(ActionAttribute.ID);
1563        }
1564    
1565        /**
1566         * Looks for the XML {@link Element} representation of the action 
1567         * associated with {@code actionId}.
1568         * 
1569         * <p>Overridden in McIDAS-V so that we can fool the IDV into working with
1570         * our icons that allow for multiple {@literal "styles"}.
1571         * 
1572         * @param actionId ID of the action whose {@literal "action node"} is desired. Cannot be {@code null}.
1573         * 
1574         * @return {@literal "action node"} associated with {@code actionId}.
1575         * 
1576         * @throws NullPointerException if {@code actionId} is {@code null}.
1577         */
1578        @Override public Element getActionNode(final String actionId) {
1579            Contract.notNull(actionId, "Null action id strings are invalid");
1580            return idvActions.getElementForAction(actionId);
1581        }
1582    
1583        /**
1584         * Searches for an action identified by a given {@code actionId}, and 
1585         * returns the value associated with its {@code attr}.
1586         * 
1587         * <p>Overridden in McIDAS-V so that we can fool the IDV into working with
1588         * our icons that allow for multiple {@literal "styles"}.
1589         * 
1590         * @param actionId ID of the action whose attribute value is desired. Cannot be {@code null}.
1591         * @param attr The attribute whose value is desired. Cannot be {@code null}.
1592         * 
1593         * @return Value associated with the given action and given attribute.
1594         * 
1595         * @throws NullPointerException if {@code actionId} or {@code attr} is {@code null}.
1596         */
1597        @Override public String getActionAttr(final String actionId, 
1598            final String attr) 
1599        {
1600            Contract.notNull(actionId, "Null action id strings are invalid");
1601            Contract.notNull(attr, "Null attributes are invalid");
1602            ActionAttribute actionAttr = ActionAttribute.valueOf(attr.toUpperCase());
1603            return idvActions.getAttributeForAction(stripAction(actionId), actionAttr);
1604        }
1605    
1606        /**
1607         * Attempts to verify that {@code element} represents a {@literal "valid"}
1608         * IDV action.
1609         * 
1610         * @param element {@link Element} to check. {@code null} values permitted, 
1611         * but they return {@code false}.
1612         * 
1613         * @return {@code true} if {@code element} had all required 
1614         * {@link ActionAttribute}s. {@code false} otherwise, or if 
1615         * {@code element} is {@code null}.
1616         */
1617        private static boolean isValidIdvAction(final Element element) {
1618            if (element == null)
1619                return false;
1620            for (ActionAttribute attribute : ActionAttribute.values()) {
1621                if (!attribute.isRequired())
1622                    continue;
1623                if (!XmlUtil.hasAttribute(element, attribute.asIdvString()))
1624                    return false;
1625            }
1626            return true;
1627        }
1628    
1629        /**
1630         * Builds a {@link Map} of {@link ActionAttribute}s to values for a given
1631         * {@link Element}. If {@code element} does not contain an optional attribute,
1632         * use the attribute's default value.
1633         * 
1634         * @param element {@literal "Action node"} of interest. {@code null} 
1635         * permitted, but results in an empty {@code Map}.
1636         * 
1637         * @return Mapping of {@code ActionAttribute}s to values, or an empty 
1638         * {@code Map} if {@code element} is {@code null}.
1639         */
1640        private static Map<ActionAttribute, String> actionElementToMap(
1641            final Element element) 
1642        {
1643            if (element == null)
1644                return Collections.emptyMap();
1645            // loop through set of action attributes; if element contains attribute "A", add it; return results.
1646            Map<ActionAttribute, String> attrs = 
1647                new LinkedHashMap<ActionAttribute, String>();
1648            for (ActionAttribute attribute : ActionAttribute.values()) {
1649                String idvStr = attribute.asIdvString();
1650                if (XmlUtil.hasAttribute(element, idvStr))
1651                    attrs.put(attribute, XmlUtil.getAttribute(element, idvStr));
1652                else
1653                    attrs.put(attribute, attribute.defaultValue());
1654            }
1655            return attrs;
1656        }
1657    
1658        /**
1659         * <p>
1660         * Builds a tree out of the bundles that should appear within the McV
1661         * toolbar. A tree is a nice way to store this data, as the default IDV
1662         * behavior is to act kinda like a file explorer when it displays these
1663         * bundles.
1664         * </p>
1665         * 
1666         * <p>
1667         * The tree makes it REALLY easy to replicate the default IDV
1668         * functionality.
1669         * </p>
1670         * 
1671         * @return The root BundleTreeNode for the tree containing toolbar bundles.
1672         */
1673        public BundleTreeNode buildBundleTree() {
1674            // handy reference to parent nodes
1675            Hashtable<String, BundleTreeNode> mapper =
1676                new Hashtable<String, BundleTreeNode>();
1677    
1678            final String TOOLBAR = "Toolbar";
1679    
1680            int bundleType = IdvPersistenceManager.BUNDLES_FAVORITES;
1681    
1682            final List<SavedBundle> bundles =
1683                getPersistenceManager().getBundles(bundleType);
1684    
1685            // iterate through all toolbar bundles
1686            for (SavedBundle bundle : bundles) {
1687                String categoryPath = "";
1688                String lastCategory = "";
1689                String grandParentPath = "";
1690    
1691                // build the "path" to the bundle. these paths basically look like
1692                // "Toolbar>category>subcategory>." so "category" is a category of
1693                // toolbar bundles and subcategory is a subcategory of that. The
1694                // IDV will build nice JPopupMenus with everything that appears in
1695                // "category," so McV needs to do the same thing. thus McV needs to
1696                // figure out the complete path to each toolbar bundle!
1697                List<String> categories = bundle.getCategories();
1698                if (categories != null && categories.size() > 0
1699                    && categories.get(0).equals(TOOLBAR) == false)
1700                    continue;
1701    
1702                for (String category : categories) {
1703                    grandParentPath = categoryPath;
1704                    categoryPath += category + ">";
1705                    lastCategory = category;
1706                }
1707    
1708                // if the current path hasn't been encountered yet there is some
1709                // work to do.
1710                if (mapper.containsKey(categoryPath) == false) {
1711                    // create the "parent" node for this bundle. note that no
1712                    // SavedBundle is stored for parent nodes!
1713                    BundleTreeNode newParent = new BundleTreeNode(lastCategory);
1714    
1715                    // make sure that we store the fact that we've seen this path
1716                    mapper.put(categoryPath, newParent);
1717    
1718                    // also need to add newParent to grandparent's kids!
1719                    if (lastCategory.equals(TOOLBAR) == false) {
1720                        BundleTreeNode grandParent = mapper.get(grandParentPath);
1721                        grandParent.addChild(newParent);
1722                    }
1723                }
1724    
1725                // so the tree book-keeping (if any) is done and we can just add
1726                // the current SavedBundle to its parent node within the tree.
1727                BundleTreeNode parent = mapper.get(categoryPath);
1728                parent.addChild(new BundleTreeNode(bundle.getName(), bundle));
1729            }
1730    
1731            // return the root of the tree.
1732            return mapper.get("Toolbar>");
1733        }
1734    
1735        /**
1736         * Recursively builds the contents of the (first call) JPopupMenu. This is
1737         * where that tree annoyance stuff comes in handy. This is basically a
1738         * simple tree traversal situation.
1739         * 
1740         * @param node The node that we're trying to use to build the contents.
1741         * @param comp The component to which we add node contents.
1742         */
1743        private void buildPopupMenu(BundleTreeNode node, JComponent comp) {
1744            // if the current node has no bundle, it's considered a parent node
1745            if (node.getBundle() == null) {
1746                // parent nodes mean that we have to create a JMenu and add it
1747                JMenu test = new JMenu(node.getName());
1748                comp.add(test);
1749    
1750                // recurse through children to continue building.
1751                for (BundleTreeNode kid : node.getChildren())
1752                    buildPopupMenu(kid, test);
1753    
1754            } else {
1755                // nodes with bundles can simply be added to the JMenu
1756                // (or JPopupMenu)
1757                JMenuItem mi = new JMenuItem(node.getName());
1758                final SavedBundle theBundle = node.getBundle();
1759    
1760                mi.addActionListener(new ActionListener() {
1761                    public void actionPerformed(ActionEvent ae) {
1762                        //Do it in a thread
1763                        Misc.run(UIManager.this, "processBundle", theBundle);
1764                    }
1765                });
1766    
1767                comp.add(mi);
1768            }
1769        }
1770    
1771        @Override
1772        public void initDone() {
1773            super.initDone();
1774            if (getStore().get(Constants.PREF_VERSION_CHECK, true)) {
1775                    StateManager stateManager = (StateManager) getStateManager();
1776                    stateManager.checkForNewerVersion(false);
1777                    stateManager.checkForNotice(false);
1778            }
1779            
1780            // not super excited about how this works.
1781    //      showBasicWindow(true);
1782            
1783            initDone = true;
1784            
1785            showDashboard();
1786        }
1787    
1788        /**
1789         * Create the splash screen if needed
1790         */
1791        public void initSplash() {
1792            if (getProperty(PROP_SHOWSPLASH, true)
1793                    && !getArgsManager().getNoGui()
1794                    && !getArgsManager().getIsOffScreen()
1795                    && !getArgsManager().testMode) {
1796                splash = new McvSplash(idv);
1797                splashMsg("Loading Programs");
1798            }
1799        }
1800        
1801        /**
1802         *  Create (if null)  and show the HelpTipDialog. If checkPrefs is true
1803         *  then only create the dialog if the PREF_HELPTIPSHOW preference is true.
1804         *
1805         * @param checkPrefs Should the user preferences be checked
1806         */
1807        /** THe help tip dialog */
1808        private McvHelpTipDialog helpTipDialog;
1809        public void initHelpTips(boolean checkPrefs) {
1810            try {
1811                if (getIdv().getArgsManager().getIsOffScreen()) {
1812                    return;
1813                }
1814                if (checkPrefs) {
1815                    if ( !getStore().get(McvHelpTipDialog.PREF_HELPTIPSHOW, true)) {
1816                        return;
1817                    }
1818                }
1819                if (helpTipDialog == null) {
1820                    IdvResourceManager resourceManager = getResourceManager();
1821                    helpTipDialog = new McvHelpTipDialog(
1822                        resourceManager.getXmlResources(
1823                            resourceManager.RSC_HELPTIPS), getIdv(), getStore(),
1824                                getIdvClass(),
1825                                getStore().get(
1826                                    McvHelpTipDialog.PREF_HELPTIPSHOW, true));
1827                }
1828                helpTipDialog.setVisible(true);
1829                GuiUtils.toFront(helpTipDialog);
1830            } catch (Throwable excp) {
1831                logException("Reading help tips", excp);
1832            }
1833        }
1834        /**
1835         *  If created, close the HelpTipDialog window.
1836         */
1837        public void closeHelpTips() {
1838            if (helpTipDialog != null) {
1839                helpTipDialog.setVisible(false);
1840            }
1841        }
1842        /**
1843         *  Create (if null)  and show the HelpTipDialog
1844         */
1845        public void showHelpTips() {
1846            initHelpTips(false);
1847        }
1848    
1849        /**
1850         * Populate a menu with bundles known to the <tt>PersistenceManager</tt>.
1851         * @param inBundleMenu The menu to populate
1852         */
1853        public void makeBundleMenu(JMenu inBundleMenu) {
1854            final int bundleType = IdvPersistenceManager.BUNDLES_FAVORITES;
1855    
1856            JMenuItem mi;
1857            mi = new JMenuItem("Manage...");
1858            McVGuiUtils.setMenuImage(mi, Constants.ICON_FAVORITEMANAGE_SMALL);
1859            mi.setMnemonic(GuiUtils.charToKeyCode("M"));
1860            inBundleMenu.add(mi);
1861            mi.addActionListener(new ActionListener() {
1862                public void actionPerformed(ActionEvent ae) {
1863                    showBundleDialog(bundleType);
1864                }
1865            });
1866    
1867            final List bundles = getPersistenceManager().getBundles(bundleType);
1868            if (bundles.size() == 0) {
1869                return;
1870            }
1871            final String title =
1872                getPersistenceManager().getBundleTitle(bundleType);
1873            final String bundleDir =
1874                getPersistenceManager().getBundleDirectory(bundleType);
1875    
1876            JMenu bundleMenu = new JMenu(title);
1877            McVGuiUtils.setMenuImage(bundleMenu, Constants.ICON_FAVORITE_SMALL);
1878            bundleMenu.setMnemonic(GuiUtils.charToKeyCode(title));
1879    
1880    //        getPersistenceManager().initBundleMenu(bundleType, bundleMenu);
1881    
1882            Hashtable catMenus = new Hashtable();
1883            inBundleMenu.addSeparator();
1884            inBundleMenu.add(bundleMenu);
1885            for (int i = 0; i < bundles.size(); i++) {
1886                SavedBundle bundle       = (SavedBundle) bundles.get(i);
1887                List        categories   = bundle.getCategories();
1888                JMenu       catMenu      = bundleMenu;
1889                String      mainCategory = "";
1890                for (int catIdx = 0; catIdx < categories.size(); catIdx++) {
1891                    String category = (String) categories.get(catIdx);
1892                    mainCategory += "." + category;
1893                    JMenu tmpMenu = (JMenu) catMenus.get(mainCategory);
1894                    if (tmpMenu == null) {
1895                        tmpMenu = new JMenu(category);
1896                        catMenu.add(tmpMenu);
1897                        catMenus.put(mainCategory, tmpMenu);
1898                    }
1899                    catMenu = tmpMenu;
1900    
1901                }
1902    
1903                final SavedBundle theBundle = bundle;
1904                mi = new JMenuItem(bundle.getName());
1905                mi.addActionListener(new ActionListener() {
1906                    public void actionPerformed(ActionEvent ae) {
1907                        //Do it in a thread
1908                        Misc.run(UIManager.this, "processBundle", theBundle);
1909                    }
1910                });
1911                catMenu.add(mi);
1912            }
1913        }
1914    
1915        /**
1916         * Overridden to build a custom Window menu.
1917         * @see ucar.unidata.idv.ui.IdvUIManager#makeWindowsMenu(JMenu, IdvWindow)
1918         */
1919        @Override public void makeWindowsMenu(final JMenu windowMenu, final IdvWindow idvWindow) {
1920            JMenuItem mi;
1921            boolean first = true;
1922    
1923            mi = new JMenuItem("Show Data Explorer");
1924            McVGuiUtils.setMenuImage(mi, Constants.ICON_DATAEXPLORER_SMALL);
1925            mi.addActionListener(this);
1926            mi.setActionCommand(ACT_SHOW_DASHBOARD);
1927            windowMenu.add(mi);
1928    
1929            makeTabNavigationMenu(windowMenu);
1930    
1931            @SuppressWarnings("unchecked") // it's how the IDV does it.
1932            List windows = new ArrayList(IdvWindow.getWindows());
1933            for (int i = 0; i < windows.size(); i++) {
1934                final IdvWindow window = ((IdvWindow)windows.get(i));
1935    
1936                // Skip the main window
1937                if (window.getIsAMainWindow())
1938                    continue;
1939    
1940                String title = window.getTitle();
1941                String titleParts[] = splitTitle(title);
1942    
1943                if (titleParts.length == 2)
1944                    title = titleParts[1];
1945    
1946                // Skip the data explorer and display controller
1947                String dataSelectorNameParts[] = splitTitle(Constants.DATASELECTOR_NAME);
1948                if (title.equals(Constants.DATASELECTOR_NAME) || title.equals(dataSelectorNameParts[1]))
1949                    continue;
1950    
1951                // Add a meaningful name if there is none
1952                if (title.equals(""))
1953                    title = "<Unnamed>";
1954    
1955                if (window.isVisible()) {
1956                    mi = new JMenuItem(title);
1957                    mi.addActionListener(new ActionListener() {
1958                        public void actionPerformed(ActionEvent ae) {
1959                            window.toFront();
1960                        }
1961                    });
1962    
1963                    if (first) {
1964                        windowMenu.addSeparator();
1965                        first = false;
1966                    }
1967    
1968                    windowMenu.add(mi);
1969                }
1970            }
1971    
1972            
1973            Msg.translateTree(windowMenu);
1974        }
1975    
1976        /**
1977         * 
1978         * @param menu
1979         */
1980        private void makeTabNavigationMenu(final JMenu menu) {
1981            if (!didInitActions) {
1982                didInitActions = true;
1983                initTabNavActions();
1984            }
1985    
1986            if (McVGuiUtils.getAllComponentHolders().size() <= 1)
1987                return;
1988    
1989            menu.addSeparator();
1990    
1991            menu.add(new JMenuItem(nextDisplayAction));
1992            menu.add(new JMenuItem(prevDisplayAction));
1993            menu.add(new JMenuItem(showDisplayAction));
1994    
1995            if (McVGuiUtils.getAllComponentGroups().size() > 0)
1996                menu.addSeparator();
1997    
1998            Msg.translateTree(menu);
1999        }
2000        
2001        /**
2002         * Add in the dynamic menu for displaying formulas
2003         *
2004         * @param menu edit menu to add to
2005         */
2006        public void makeFormulasMenu(JMenu menu) {
2007            GuiUtils.makeMenu(menu, getJythonManager().doMakeFormulaDataSourceMenuItems(null));
2008        }
2009        
2010        /** Whether or not the list of available actions has been initialized. */
2011        private boolean didInitActions = false;
2012    
2013        /** Key combo for the popup with list of displays. */
2014        private ShowDisplayAction showDisplayAction;
2015    
2016        /** 
2017         * Key combo for moving to the previous display relative to the current. For
2018         * key combos the lists of displays in the current window is circular.
2019         */
2020        private PrevDisplayAction prevDisplayAction;
2021    
2022        /** 
2023         * Key combo for moving to the next display relative to the current. For
2024         * key combos the lists of displays in the current window is circular.
2025         */
2026        private NextDisplayAction nextDisplayAction;
2027    
2028        /** Modifier key, like &quot;control&quot; or &quot;shift&quot;. */
2029        private static final String PROP_KB_MODIFIER = "mcidasv.tabbedui.display.kbmodifier";
2030    
2031        /** Key that pops up the list of displays. Used in conjunction with <code>PROP_KB_MODIFIER</code>. */
2032        private static final String PROP_KB_SELECT_DISPLAY = "mcidasv.tabbedui.display.kbselect";
2033        
2034        /** Key for moving to the previous display. Used in conjunction with <code>PROP_KB_MODIFIER</code>. */
2035        private static final String PROP_KB_DISPLAY_PREV = "mcidasv.tabbedui.display.kbprev";
2036    
2037        /** Key for moving to the next display. Used in conjunction with <code>PROP_KB_MODIFIER</code>. */
2038        private static final String PROP_KB_DISPLAY_NEXT = "mcidasv.tabbedui.display.kbnext";
2039    
2040        /** Key for showing the dashboard. Used in conjunction with <code>PROP_KB_MODIFIER</code>. */
2041        private static final String PROP_KB_SHOW_DASHBOARD = "mcidasv.tabbedui.display.kbdashboard";
2042    
2043        // TODO: make all this stuff static: mod + acc don't need to read the properties file.
2044        // look at: http://community.livejournal.com/jkff_en/341.html
2045        // look at: effective java, particularly the stuff about enums
2046        private void initTabNavActions() {
2047            String mod = idv.getProperty(PROP_KB_MODIFIER, "control") + " ";
2048            String acc = idv.getProperty(PROP_KB_SELECT_DISPLAY, "D");
2049    
2050            String stroke = mod + acc;
2051            showDisplayAction = new ShowDisplayAction(KeyStroke.getKeyStroke(stroke));
2052    
2053            acc = idv.getProperty(PROP_KB_DISPLAY_PREV, "P");
2054            stroke = mod + acc;
2055            prevDisplayAction = new PrevDisplayAction(KeyStroke.getKeyStroke(stroke));
2056    
2057            acc = idv.getProperty(PROP_KB_DISPLAY_NEXT, "N");
2058            stroke = mod + acc;
2059            nextDisplayAction = new NextDisplayAction(KeyStroke.getKeyStroke(stroke));
2060        }
2061    
2062        /**
2063         * Add all the show window keyboard shortcuts. To make keyboard shortcuts
2064         * global, i.e., available no matter what window is active, the appropriate 
2065         * actions have to be added the the window contents action and input maps.
2066         * 
2067         * FIXME: This can't be the right way to do this!
2068         * 
2069         * @param window IdvWindow that requires keyboard shortcut capability.
2070         */
2071        private void initDisplayShortcuts(IdvWindow window) {
2072            JComponent jcomp = window.getContents();
2073            jcomp.getActionMap().put("show_disp", showDisplayAction);
2074            jcomp.getActionMap().put("prev_disp", prevDisplayAction);
2075            jcomp.getActionMap().put("next_disp", nextDisplayAction);
2076            jcomp.getActionMap().put("show_dashboard", new AbstractAction() {
2077                private static final long serialVersionUID = -364947940824325949L;
2078                public void actionPerformed(ActionEvent evt) {
2079                    showDashboard();
2080                }
2081            });
2082    
2083            String mod = getIdv().getProperty(PROP_KB_MODIFIER, "control");
2084            String acc = getIdv().getProperty(PROP_KB_SELECT_DISPLAY, "d");
2085            jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2086                KeyStroke.getKeyStroke(mod + " " + acc),
2087                "show_disp"
2088            );
2089    
2090            acc = getIdv().getProperty(PROP_KB_SHOW_DASHBOARD, "MINUS");
2091            jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2092                KeyStroke.getKeyStroke(mod + " " + acc),
2093                "show_dashboard"
2094            );
2095    
2096            acc = getIdv().getProperty(PROP_KB_DISPLAY_NEXT, "N");
2097            jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2098                KeyStroke.getKeyStroke(mod + " " + acc),
2099                "next_disp"
2100            );
2101    
2102            acc = getIdv().getProperty(PROP_KB_DISPLAY_PREV, "P");
2103            jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2104                KeyStroke.getKeyStroke(mod + " " + acc),
2105                "prev_disp"
2106            );
2107        }
2108    
2109        /**
2110         * Show Bruce's display selector widget.
2111         */
2112        protected void showDisplaySelector() {
2113            IdvWindow mainWindow = IdvWindow.getActiveWindow();
2114            JPanel contents = new JPanel();
2115            contents.setLayout(new BorderLayout());
2116            JComponent comp = getDisplaySelectorComponent();
2117            final JDialog dialog = new JDialog(mainWindow.getFrame(), "Select Display", true);
2118            dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
2119            contents.add(comp, BorderLayout.CENTER);
2120            JButton button = new JButton("OK");
2121            button.addActionListener(new ActionListener() {
2122                public void actionPerformed(ActionEvent evt) {
2123                    final ViewManager vm = getVMManager().getLastActiveViewManager();
2124                    // final DisplayProps disp = getDisplayProps(vm);
2125                    // if (disp != null)
2126                    //    showDisplay(disp);
2127                    final McvComponentHolder holder = (McvComponentHolder)getViewManagerHolder(vm);
2128                    if (holder != null)
2129                        holder.setAsActiveTab();
2130                    
2131                    // have to do this on the event dispatch thread so we make
2132                    // sure it happens after showDisplay
2133                    SwingUtilities.invokeLater(new Runnable() {
2134                        public void run() {
2135                            //setActiveDisplay(disp, disp.managers.indexOf(vm));
2136                            if (holder != null)
2137                                getVMManager().setLastActiveViewManager(vm);
2138                        }
2139                    });
2140    
2141                    dialog.dispose();
2142                }
2143            });
2144            JPanel buttonPanel = new JPanel();
2145            buttonPanel.add(button);
2146            dialog.add(buttonPanel, BorderLayout.AFTER_LAST_LINE);
2147            JScrollPane scroller = new JScrollPane(contents);
2148            scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
2149            scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
2150            dialog.add(scroller, BorderLayout.CENTER);
2151            dialog.setSize(200, 300);
2152            dialog.setLocationRelativeTo(mainWindow.getFrame());
2153            dialog.setVisible(true);
2154        }
2155    
2156        private class ShowDisplayAction extends AbstractAction {
2157            private static final long serialVersionUID = -4609753725057124244L;
2158            private static final String ACTION_NAME = "Select Display...";
2159            public ShowDisplayAction(KeyStroke k) {
2160                super(ACTION_NAME);
2161                putValue(Action.ACCELERATOR_KEY, k);
2162            }
2163    
2164            public void actionPerformed(ActionEvent e) {
2165                String cmd = e.getActionCommand();
2166                if (cmd == null)
2167                    return;
2168    
2169                if (ACTION_NAME.equals(cmd)) {
2170                    showDisplaySelector();
2171                } else {
2172                    List<IdvComponentHolder> holders = McVGuiUtils.getAllComponentHolders();
2173                    McvComponentHolder holder = null;
2174                    int index = 0;
2175                    try {
2176                        index = Integer.parseInt(cmd) - 1;
2177                        holder = (McvComponentHolder)holders.get(index);
2178                    } catch (Exception ex) {}
2179    
2180                    if (holder != null)
2181                        holder.setAsActiveTab();
2182                }
2183            }
2184        }
2185    
2186        private class PrevDisplayAction extends AbstractAction {
2187            private static final long serialVersionUID = -3551890663976755671L;
2188            private static final String ACTION_NAME = "Previous Display";
2189    
2190            public PrevDisplayAction(KeyStroke k) {
2191                super(ACTION_NAME);
2192                putValue(Action.ACCELERATOR_KEY, k);
2193            }
2194    
2195            public void actionPerformed(ActionEvent e) {
2196                McvComponentHolder prev = (McvComponentHolder)McVGuiUtils.getBeforeActiveHolder();
2197                if (prev != null)
2198                    prev.setAsActiveTab();
2199            }
2200        }
2201    
2202        private class NextDisplayAction extends AbstractAction {
2203            private static final long serialVersionUID = 5431901451767117558L;
2204            private static final String ACTION_NAME = "Next Display";
2205    
2206            public NextDisplayAction(KeyStroke k) {
2207                super(ACTION_NAME);
2208                putValue(Action.ACCELERATOR_KEY, k);
2209            }
2210    
2211            public void actionPerformed(ActionEvent e) {
2212                McvComponentHolder next = (McvComponentHolder)McVGuiUtils.getAfterActiveHolder();
2213                if (next != null)
2214                    next.setAsActiveTab();
2215            }
2216        }
2217    
2218        /**
2219         * Populate a "new display" menu from the available skin list. Many thanks
2220         * to Bruce for doing this in the venerable TabbedUIManager.
2221         * 
2222         * @param newDisplayMenu menu to populate.
2223         * @param inWindow Is the skinned display to be created in a window?
2224         * 
2225         * @see ucar.unidata.idv.IdvResourceManager#RSC_SKIN
2226         * 
2227         * @return Menu item populated with display skins
2228         */
2229        protected JMenuItem doMakeNewDisplayMenu(JMenuItem newDisplayMenu, 
2230            final boolean inWindow) 
2231        {
2232            if (newDisplayMenu != null) {
2233    
2234                String skinFilter = "idv.skin";
2235                if (!inWindow) {
2236                    skinFilter = "mcv.skin";
2237                }
2238    
2239                final XmlResourceCollection skins =
2240                    getResourceManager().getXmlResources(
2241                        IdvResourceManager.RSC_SKIN);
2242    
2243                Map<String, JMenu> menus = new Hashtable<String, JMenu>();
2244                for (int i = 0; i < skins.size(); i++) {
2245                    final Element root = skins.getRoot(i);
2246                    if (root == null) {
2247                        continue;
2248                    }
2249    
2250                    // filter out mcv or idv skins based on whether or not we're
2251                    // interested in tabs or new windows.
2252                    final String skinid = skins.getProperty("skinid", i);
2253                    if (skinid != null && skinid.startsWith(skinFilter)) {
2254                        continue;
2255                    }
2256    
2257                    final int skinIndex = i;
2258                    List<String> names =
2259                        StringUtil.split(skins.getShortName(i), ">", true, true);
2260    
2261                    JMenuItem theMenu = newDisplayMenu;
2262                    String path = "";
2263                    for (int nameIdx = 0; nameIdx < names.size() - 1; nameIdx++) {
2264                        String catName = names.get(nameIdx);
2265                        path = path + ">" + catName;
2266                        JMenu tmpMenu = menus.get(path);
2267                        if (tmpMenu == null) {
2268                            tmpMenu = new JMenu(catName);
2269                            theMenu.add(tmpMenu);
2270                            menus.put(path, tmpMenu);
2271                        }
2272                        theMenu = tmpMenu;
2273                    }
2274    
2275                    final String name = names.get(names.size() - 1);
2276    
2277                    IdvWindow window = IdvWindow.getActiveWindow();
2278                    for (final McvComponentGroup group : McVGuiUtils.idvGroupsToMcv(window)) {
2279                        JMenuItem mi = new JMenuItem(name);
2280    
2281                        mi.addActionListener(new ActionListener() {
2282    
2283                            public void actionPerformed(ActionEvent ae) {
2284                                if (!inWindow) {
2285                                    group.makeSkin(skinIndex);
2286                                } else {
2287                                    createNewWindow(null, true,
2288                                        getStateManager().getTitle(), skins.get(
2289                                            skinIndex).toString(), skins.getRoot(
2290                                            skinIndex, false), inWindow, null);
2291                                }
2292                            }
2293                        });
2294                        theMenu.add(mi);
2295                    }
2296                }
2297    
2298                // attach the dynamic skin menu item to the tab menu.
2299    //            if (!inWindow) {
2300    //                ((JMenu)newDisplayMenu).addSeparator();
2301    //                IdvWindow window = IdvWindow.getActiveWindow();
2302    //
2303    //                final McvComponentGroup group =
2304    //                    (McvComponentGroup)window.getComponentGroups().get(0);
2305    //
2306    //                JMenuItem mi = new JMenuItem("Choose Your Own Adventure...");
2307    //                mi.addActionListener(new ActionListener() {
2308    //
2309    //                    public void actionPerformed(ActionEvent e) {
2310    //                        makeDynamicSkin(group);
2311    //                    }
2312    //                });
2313    //                newDisplayMenu.add(mi);
2314    //            }
2315            }
2316            return newDisplayMenu;
2317        }
2318    
2319        // for the time being just create some basic viewmanagers.
2320    //    public void makeDynamicSkin(McvComponentGroup group) {
2321    //        // so I have my megastring (which I hate--a class that can generate XML would be cooler) (though it would boil down to the same thing...)
2322    //        try {
2323    //            Document doc = XmlUtil.getDocument(SKIN_TEMPLATE);
2324    //            Element root = doc.getDocumentElement();
2325    //            Element rightChild = doc.createElement("idv.view");
2326    //            rightChild.setAttribute("class", "ucar.unidata.idv.TransectViewManager");
2327    //            rightChild.setAttribute("viewid", "viewright1337");
2328    //            rightChild.setAttribute("id", "viewright");
2329    //            rightChild.setAttribute("properties", "name=Panel 1;clickToFocus=true;showToolBars=true;shareViews=true;showControlLegend=false;initialSplitPaneLocation=0.2;legendOnLeft=true;size=300:400;shareGroup=view%versionuid%;");
2330    //
2331    //            Element leftChild = doc.createElement("idv.view");
2332    //            leftChild.setAttribute("class", "ucar.unidata.idv.MapViewManager");
2333    //            leftChild.setAttribute("viewid", "viewleft1337");
2334    //            leftChild.setAttribute("id", "viewleft");
2335    //            leftChild.setAttribute("properties", "name=Panel 2;clickToFocus=true;showToolBars=true;shareViews=true;showControlLegend=false;size=300:400;shareGroup=view%versionuid%;");
2336    //
2337    //            Element startNode = XmlUtil.findElement(root, "splitpane", "embeddednode", "true");
2338    //            startNode.appendChild(rightChild);
2339    //            startNode.appendChild(leftChild);
2340    //            group.makeDynamicSkin(root);
2341    //        } catch (Exception e) {
2342    //            LogUtil.logException("Error: parsing skin template:", e);
2343    //        }
2344    //    }
2345    //
2346    //    private static final String SKIN_TEMPLATE = 
2347    //        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
2348    //        "<skin embedded=\"true\">\n" +
2349    //        "  <ui>\n" +
2350    //        "    <panel layout=\"border\" bgcolor=\"red\">\n" +
2351    //        "      <idv.menubar place=\"North\"/>\n" +
2352    //        "      <panel layout=\"border\" place=\"Center\">\n" +
2353    //        "        <panel layout=\"flow\" place=\"North\">\n" +
2354    //        "          <idv.toolbar id=\"idv.toolbar\" place=\"West\"/>\n" +
2355    //        "          <panel id=\"idv.favoritesbar\" place=\"North\"/>\n" +
2356    //        "        </panel>\n" +
2357    //        "        <splitpane embeddednode=\"true\" resizeweight=\"0.5\" onetouchexpandable=\"true\" orientation=\"h\" bgcolor=\"blue\" layout=\"grid\" cols=\"2\" place=\"Center\">\n" +
2358    //        "        </splitpane>\n" +
2359    //        "      </panel>\n" +
2360    //        "      <component idref=\"bottom_bar\"/>\n" +
2361    //        "    </panel>\n" +
2362    //        "  </ui>\n" +
2363    //        "  <styles>\n" +
2364    //        "    <style class=\"iconbtn\" space=\"2\" mouse_enter=\"ui.setText(idv.messagelabel,prop:tooltip);ui.setBorder(this,etched);\" mouse_exit=\"ui.setText(idv.messagelabel,);ui.setBorder(this,button);\"/>\n" +
2365    //        "    <style class=\"textbtn\" space=\"2\" mouse_enter=\"ui.setText(idv.messagelabel,prop:tooltip)\" mouse_exit=\"ui.setText(idv.messagelabel,)\"/>\n" +
2366    //        "  </styles>\n" +
2367    //        "  <components>\n" +
2368    //        "    <idv.statusbar place=\"South\" id=\"bottom_bar\"/>\n" +
2369    //        "  </components>\n" +
2370    //        "  <properties>\n" +
2371    //        "    <property name=\"icon.wait.wait\" value=\"/ucar/unidata/idv/images/wait.gif\"/>\n" +
2372    //        "  </properties>\n" +
2373    //        "</skin>\n";
2374    
2375        private int holderCount;
2376        
2377        /**
2378         * Associates a given ViewManager with a given ComponentHolder.
2379         * 
2380         * @param vm The ViewManager that is inside <tt>holder</tt>.
2381         * @param holder The ComponentHolder that contains <tt>vm</tt>.
2382         */
2383        public void setViewManagerHolder(ViewManager vm, ComponentHolder holder) {
2384            viewManagers.put(vm, holder);
2385            holderCount = getComponentHolders().size();
2386        }
2387    
2388        public Set<ComponentHolder> getComponentHolders() {
2389            return newHashSet(viewManagers.values());
2390        }
2391    
2392        public int getComponentHolderCount() {
2393            return holderCount;
2394        }
2395    
2396        public int getComponentGroupCount() {
2397            return getComponentGroups().size();
2398        }
2399    
2400        /**
2401         * Returns the ComponentHolder containing the given ViewManager.
2402         * 
2403         * @param vm The ViewManager whose ComponentHolder is needed.
2404         * 
2405         * @return Either null or the ComponentHolder.
2406         */
2407        public ComponentHolder getViewManagerHolder(ViewManager vm) {
2408            return viewManagers.get(vm);
2409        }
2410    
2411        /**
2412         * Disassociate a given ViewManager from its ComponentHolder.
2413         * 
2414         * @return The associated ComponentHolder.
2415         */
2416        public ComponentHolder removeViewManagerHolder(ViewManager vm) {
2417            ComponentHolder holder = viewManagers.remove(vm);
2418            holderCount = getComponentHolders().size();
2419            return holder;
2420        }
2421    
2422        /**
2423         * Overridden to keep the dashboard around after it's initially created.
2424         * Also give the user the ability to show a particular tab.
2425         * @see ucar.unidata.idv.ui.IdvUIManager#showDashboard()
2426         */
2427        @Override
2428        public void showDashboard() {
2429            showDashboard("");
2430        }
2431    
2432        /**
2433         * Creates the McVViewPanel component that shows up in the dashboard.
2434         */
2435        @Override
2436        protected ViewPanel doMakeViewPanel() {
2437            ViewPanel vp = new McIDASVViewPanel(idv);
2438            vp.getContents();
2439            return vp;
2440        }
2441    
2442        /**
2443         * @return A map of skin ids to their index within the skin resource.
2444         */
2445        private Map<String, Integer> readSkinIds() {
2446            Map<String, Integer> ids = new HashMap<String, Integer>();
2447            XmlResourceCollection skins = getResourceManager().getXmlResources(IdvResourceManager.RSC_SKIN);
2448            for (int i = 0; i < skins.size(); i++) {
2449                String id = skins.getProperty("skinid", i);
2450                if (id != null)
2451                    ids.put(id, i);
2452            }
2453            return ids;
2454        }
2455    
2456        /**
2457         * Adds a skinned component holder to the active component group.
2458         * 
2459         * @param skinId The value of the skin's skinid attribute.
2460         */
2461        public void createNewTab(final String skinId) {
2462            IdvComponentGroup group = 
2463                McVGuiUtils.getComponentGroup(IdvWindow.getActiveWindow());
2464    
2465            if (skinIds.containsKey(skinId))
2466                group.makeSkin(skinIds.get(skinId));
2467        }
2468    
2469        /**
2470         * Method to do the work of showing the Data Explorer (nee Dashboard)
2471         */
2472        @SuppressWarnings("unchecked") // IdvWindow.getWindows only adds IdvWindows.
2473        public void showDashboard(String tabName) {
2474            if (!initDone) {
2475                return;
2476            } else if (dashboard == null) {
2477                showWaitCursor();
2478                doMakeBasicWindows();
2479                showNormalCursor();
2480                String title = makeTitle(getStateManager().getTitle(), Constants.DATASELECTOR_NAME);
2481                for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) {
2482                    if (title.equals(window.getTitle())) {
2483                        dashboard = window;
2484                        dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
2485                    }
2486                }
2487            } else {
2488                dashboard.show();
2489            }
2490    
2491            if (tabName.equals(""))
2492                return;
2493    
2494            // Dig two panels deep looking for a JTabbedPane
2495            // If you find one, try to show the requested tab name
2496            JComponent contents = dashboard.getContents();
2497            JComponent component = (JComponent)contents.getComponent(0);
2498            JTabbedPane tPane = null;
2499            if (component instanceof JTabbedPane) {
2500                tPane = (JTabbedPane)component;
2501            }
2502            else {
2503                JComponent component2 = (JComponent)component.getComponent(0);
2504                if (component2 instanceof JTabbedPane) {
2505                    tPane = (JTabbedPane)component2;
2506                }
2507            }
2508            if (tPane != null) {
2509                for (int i=0; i<tPane.getTabCount(); i++) {
2510                    if (tabName.equals(tPane.getTitleAt(i))) {
2511                        tPane.setSelectedIndex(i);
2512                        break;
2513                    }
2514                }
2515            }
2516        }
2517    
2518        /**
2519         * Show the support request form
2520         *
2521         * @param description Default value for the description form entry
2522         * @param stackTrace The stack trace that caused this error.
2523         * @param dialog The dialog to put the gui in, if non-null.
2524         */
2525        public void showSupportForm(final String description, 
2526            final String stackTrace, final JDialog dialog) 
2527        {
2528            java.awt.EventQueue.invokeLater(new Runnable() {
2529                public void run() {
2530                    // TODO: mcvstatecollector should have a way to gather the
2531                    // exception information..
2532                    McIDASV mcv = (McIDASV)getIdv();
2533                    new SupportForm(getStore(), new McvStateCollector(mcv)).setVisible(true);
2534                }
2535            });
2536        }
2537    
2538        /**
2539         * Attempts to locate and display a dashboard component using an ID.
2540         * 
2541         * @param id ID of the desired component.
2542         * 
2543         * @return True if <code>id</code> corresponds to a component. False otherwise.
2544         */
2545        public boolean showDashboardComponent(String id) {
2546            Object comp = findComponent(id);
2547            if (comp != null) {
2548                    GuiUtils.showComponentInTabs((JComponent)comp);
2549                    return true;
2550            } else {
2551                    super.showDashboard();
2552                    for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) {
2553                            String title = makeTitle(
2554                                    getStateManager().getTitle(),
2555                                    Constants.DATASELECTOR_NAME
2556                            );
2557                            if (title.equals(window.getTitle())) {
2558                                    dashboard = window;
2559                                    dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
2560                            }
2561                    }
2562            }
2563            return false;
2564        }
2565    
2566        /**
2567         * Close and dispose of the splash window (if it has been created).
2568         */
2569        public void splashClose() {
2570            if (splash != null)
2571                splash.doClose();
2572        }
2573    
2574        /**
2575         * Show a message in the splash screen (if it exists)
2576         *
2577         * @param m The message to show
2578         */
2579        public void splashMsg(String m) {
2580            if (splash != null)
2581                splash.splashMsg(m);
2582        }
2583    
2584        /**
2585         * <p>
2586         * Uses a given toolbar editor to repopulate all toolbars so that they 
2587         * correspond to the user's choice of actions.
2588         * </p>
2589         * 
2590         * @param tbe The toolbar editor that contains the actions the user wants.
2591         */
2592        public void setCurrentToolbars(final McvToolbarEditor tbe) {
2593            List<TwoFacedObject> tfos = tbe.getTLP().getCurrentEntries();
2594            List<String> buttonIds = new ArrayList<String>();
2595            for (TwoFacedObject tfo : tfos) {
2596                if (McvToolbarEditor.isSpace(tfo))
2597                    buttonIds.add((String)null);
2598                else
2599                    buttonIds.add(TwoFacedObject.getIdString(tfo));
2600            }
2601    
2602            cachedButtons = buttonIds;
2603    
2604            for (JToolBar toolbar : toolbars) {
2605                toolbar.setVisible(false);
2606                populateToolbar(toolbar);
2607                toolbar.setVisible(true);
2608            }
2609        }
2610    
2611        /**
2612         * Append a string and object to the buffer
2613         *
2614         * @param sb  StringBuffer to append to
2615         * @param name  Name of the object
2616         * @param value  the object value
2617         */
2618        private void append(StringBuffer sb, String name, Object value) {
2619            sb.append("<b>").append(name).append("</b>: ").append(value).append("<br>");
2620        }
2621    
2622        private JMenuItem makeControlDescriptorItem(ControlDescriptor cd) {
2623            JMenuItem mi = new JMenuItem();
2624            if (cd != null) {
2625                mi = new JMenuItem(cd.getLabel());
2626                mi.addActionListener(new ObjectListener(cd) {
2627                    public void actionPerformed(ActionEvent ev) {
2628                        idv.doMakeControl(new ArrayList(),
2629                            (ControlDescriptor)theObject);
2630                    }
2631                });
2632            }
2633            return mi;
2634        }
2635    
2636        /* (non-javadoc)
2637         * Overridden so that the toolbar will update upon saving a bundle.
2638         */
2639        @Override public void displayTemplatesChanged() {
2640            super.displayTemplatesChanged();
2641            for (JToolBar toolbar : toolbars) {
2642                toolbar.setVisible(false);
2643                populateToolbar(toolbar);
2644                toolbar.setVisible(true);
2645            }
2646        }
2647    
2648        /**
2649         * Show the support request form in a non-swing thread. We do this because we cannot
2650         * call the HttpFormEntry.showUI from a swing thread
2651         *
2652         * @param description Default value for the description form entry
2653         * @param stackTrace The stack trace that caused this error.
2654         * @param dialog The dialog to put the gui in, if non-null.
2655         */
2656    
2657        private void showSupportFormInThread(String description,
2658                                             String stackTrace, JDialog dialog) {
2659            List<HttpFormEntry> entries = new ArrayList<HttpFormEntry>();
2660    
2661            StringBuffer extra   = new StringBuffer("<h3>McIDAS-V</h3>\n");
2662            Hashtable<String, String> table = 
2663                ((StateManager)getStateManager()).getVersionInfo();
2664            append(extra, "mcv.version.general", table.get("mcv.version.general"));
2665            append(extra, "mcv.version.build", table.get("mcv.version.build"));
2666            append(extra, "idv.version.general", table.get("idv.version.general"));
2667            append(extra, "idv.version.build", table.get("idv.version.build"));
2668    
2669            extra.append("<h3>OS</h3>\n");
2670            append(extra, "os.name", System.getProperty("os.name"));
2671            append(extra, "os.arch", System.getProperty("os.arch"));
2672            append(extra, "os.version", System.getProperty("os.version"));
2673    
2674            extra.append("<h3>Java</h3>\n");
2675            append(extra, "java.vendor", System.getProperty("java.vendor"));
2676            append(extra, "java.version", System.getProperty("java.version"));
2677            append(extra, "java.home", System.getProperty("java.home"));
2678    
2679            StringBuffer javaInfo = new StringBuffer();
2680            javaInfo.append("Java: home: " + System.getProperty("java.home"));
2681            javaInfo.append(" version: " + System.getProperty("java.version"));
2682    
2683            Class c = null;
2684            try {
2685                c = Class.forName("javax.media.j3d.VirtualUniverse");
2686                Method method = Misc.findMethod(c, "getProperties",
2687                                                new Class[] {});
2688                if (method == null) {
2689                    javaInfo.append("j3d <1.3");
2690                } else {
2691                    try {
2692                        Map m = (Map)method.invoke(c, new Object[] {});
2693                        javaInfo.append(" j3d:" + m.get("j3d.version"));
2694                        append(extra, "j3d.version", m.get("j3d.version"));
2695                        append(extra, "j3d.vendor", m.get("j3d.vendor"));
2696                        append(extra, "j3d.renderer", m.get("j3d.renderer"));
2697                    } catch (Exception exc) {
2698                        javaInfo.append(" j3d:" + "unknown");
2699                    }
2700                }
2701            } catch (ClassNotFoundException exc) {
2702                append(extra, "j3d", "none");
2703            }
2704    
2705            boolean persistCC = getStore().get("mcv.supportreq.cc", true);
2706    
2707            JCheckBox ccMyself = new JCheckBox("Send Copy of Support Request to Me", persistCC);
2708            ccMyself.addActionListener(new ActionListener() {
2709                public void actionPerformed(final ActionEvent e) {
2710                    JCheckBox cb = (JCheckBox)e.getSource();
2711                    getStore().put("mcv.supportreq.cc", cb.isSelected());
2712                }
2713            });
2714    
2715            boolean doWrap = idv.getProperty(PROP_WRAP_SUPPORT_DESC, true);
2716    
2717            HttpFormEntry descriptionEntry;
2718            HttpFormEntry nameEntry;
2719            HttpFormEntry emailEntry;
2720            HttpFormEntry orgEntry;
2721    
2722            entries.add(nameEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2723                    "form_data[fromName]", "Name:",
2724                    getStore().get(PROP_HELP_NAME, (String) null)));
2725            entries.add(emailEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2726                    "form_data[email]", "Your Email:",
2727                    getStore().get(PROP_HELP_EMAIL, (String) null)));
2728            entries.add(orgEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2729                    "form_data[organization]", "Organization:",
2730                    getStore().get(PROP_HELP_ORG, (String) null)));
2731            entries.add(new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2732                                          "form_data[subject]", "Subject:"));
2733    
2734            entries.add(
2735                new HttpFormEntry(
2736                    HttpFormEntry.TYPE_LABEL, "",
2737                    "<html>Please provide a <i>thorough</i> description of the problem you encountered:</html>"));
2738            entries.add(descriptionEntry =
2739                new FormEntry(doWrap, HttpFormEntry.TYPE_AREA,
2740                                  "form_data[description]", "Description:",
2741                                  description, 5, 30, true));
2742    
2743            entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE,
2744                                          "form_data[att_two]", "Attachment 1:", "",
2745                                          false));
2746            entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE,
2747                                          "form_data[att_three]", "Attachment 2:", "",
2748                                          false));
2749    
2750            entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2751                                          "form_data[submit]", "", "Send Email"));
2752            
2753            entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2754                                          "form_data[p_version]", "",
2755                                          getStateManager().getVersion()
2756                                          + " build date:"
2757                                          + getStateManager().getBuildDate()));
2758            entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2759                                          "form_data[opsys]", "",
2760                                          System.getProperty("os.name")));
2761            entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2762                                          "form_data[hardware]", "",
2763                                          javaInfo.toString()));
2764            
2765            JLabel topLabel = 
2766                new JLabel("<html>This form allows you to send a support request to the McIDAS Help Desk.<br></html>");
2767    
2768            JCheckBox includeBundleCbx =
2769                new JCheckBox("Include Current State as Bundle", false);
2770    
2771            List<JCheckBox> checkboxes = list(includeBundleCbx, ccMyself);
2772    
2773            boolean alreadyHaveDialog = true;
2774            if (dialog == null) {
2775                // NOTE: if the dialog is modeless you can leave alreadyHaveDialog
2776                // alone. If the dialog is modal you need to set alreadyHaveDialog
2777                // to false.
2778                // If alreadyHaveDialog is false with a modeless dialog, the later
2779                // call to HttpFormEntry.showUI will return false and break out of
2780                // the while loop without talking to the HTTP server.
2781                dialog = GuiUtils.createDialog(LogUtil.getCurrentWindow(),
2782                                               "Support Request Form", false);
2783    //            alreadyHaveDialog = false;
2784            }
2785    
2786            JLabel statusLabel = GuiUtils.cLabel(" ");
2787            JComponent bottom = GuiUtils.vbox(GuiUtils.leftVbox(checkboxes), statusLabel);
2788    
2789            while (true) {
2790                //Show form. Check if user pressed cancel.
2791                statusLabel.setText(" ");
2792                if ( !HttpFormEntry.showUI(entries, GuiUtils.inset(topLabel, 10),
2793                                           bottom, dialog, alreadyHaveDialog)) {
2794                    break;
2795                }
2796                statusLabel.setText("Posting support request...");
2797    
2798                //Save persistent state
2799                getStore().put(PROP_HELP_NAME, nameEntry.getValue());
2800                getStore().put(PROP_HELP_ORG, orgEntry.getValue());
2801                getStore().put(PROP_HELP_EMAIL, emailEntry.getValue());
2802                getStore().save();
2803    
2804                List<HttpFormEntry> entriesToPost = 
2805                    new ArrayList<HttpFormEntry>(entries);
2806    
2807                if ((stackTrace != null) && (stackTrace.length() > 0)) {
2808                    entriesToPost.remove(descriptionEntry);
2809                    String newDescription =
2810                        descriptionEntry.getValue()
2811                        + "\n\n******************\nStack trace:\n" + stackTrace;
2812                    entriesToPost.add(
2813                        new HttpFormEntry(
2814                            HttpFormEntry.TYPE_HIDDEN, "form_data[description]",
2815                            "Description:", newDescription, 5, 30, true));
2816                }
2817    
2818                try {
2819                    extra.append(idv.getPluginManager().getPluginHtml());
2820                    extra.append(getResourceManager().getHtmlView());
2821    
2822                    entriesToPost.add(new HttpFormEntry("form_data[att_extra]",
2823                        "extra.html", extra.toString().getBytes()));
2824    
2825                    if (includeBundleCbx.isSelected()) {
2826                        entriesToPost.add(
2827                            new HttpFormEntry(
2828                                "form_data[att_state]", "bundle" + Constants.SUFFIX_MCV,
2829                                idv.getPersistenceManager().getBundleXml(
2830                                    true).getBytes()));
2831                    }
2832                    entriesToPost.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 
2833                        "form_data[cc_user]", "", 
2834                        Boolean.toString(getStore().get("mcv.supportreq.cc", true))));
2835    
2836                    String[] results = 
2837                        HttpFormEntry.doPost(entriesToPost, SUPPORT_REQ_URL);
2838    
2839                    if (results[0] != null) {
2840                        GuiUtils.showHtmlDialog(
2841                            results[0], "Support Request Response - Error",
2842                            "Support Request Response - Error", null, true);
2843                        continue;
2844                    }
2845                    String html = results[1];
2846                    if (html.toLowerCase().indexOf("your email has been sent")
2847                            >= 0) {
2848                        LogUtil.userMessage("Your support request has been sent");
2849                        break;
2850                    } else if (html.toLowerCase().indexOf("required fields")
2851                               >= 0) {
2852                        LogUtil.userErrorMessage(
2853                            "<html>There was a problem submitting your request. <br>Is your email correct?</html>");
2854                    } else {
2855                        GuiUtils.showHtmlDialog(
2856                            html, "Unknown Support Request Response",
2857                            "Unknown Support Request Response", null, true);
2858                        System.err.println(html.toLowerCase());
2859                    }
2860                } catch (Exception exc) {
2861                    LogUtil.logException("Doing support request form", exc);
2862                }
2863            }
2864            dialog.dispose();
2865        }
2866    
2867        @Override protected IdvXmlUi doMakeIdvXmlUi(IdvWindow window, 
2868            List viewManagers, Element skinRoot) 
2869        {
2870            return new McIDASVXmlUi(window, viewManagers, idv, skinRoot);
2871        }
2872    
2873        /**
2874         * DeInitialize the given menu before it is shown
2875         * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu)
2876         */
2877        @Override
2878        protected void handleMenuDeSelected(final String id, final JMenu menu, final IdvWindow idvWindow) {
2879            super.handleMenuDeSelected(id, menu, idvWindow);
2880        }
2881    
2882        /**
2883         * Initialize the given menu before it is shown
2884         * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu)
2885         */
2886        @Override
2887        protected void handleMenuSelected(final String id, final JMenu menu, final IdvWindow idvWindow) {
2888            if (id.equals(MENU_NEWVIEWS)) {
2889                ViewManager last = getVMManager().getLastActiveViewManager();
2890                menu.removeAll();
2891                makeViewStateMenu(menu, last);
2892            } else if (id.equals("bundles")) {
2893                menu.removeAll();
2894                makeBundleMenu(menu);
2895            } else if (id.equals(MENU_NEWDISPLAY_TAB)) {
2896                menu.removeAll();
2897                doMakeNewDisplayMenu(menu, false);
2898            } else if (id.equals(MENU_NEWDISPLAY)) {
2899                menu.removeAll();
2900                doMakeNewDisplayMenu(menu, true);
2901            } else if (id.equals("menu.tools.projections.deletesaved")) {
2902                menu.removeAll();
2903                makeDeleteViewsMenu(menu);
2904            } else if (id.equals("file.default.layout")) {
2905                makeDefaultLayoutMenu(menu);
2906            } else if (id.equals("tools.formulas")) {
2907                menu.removeAll();
2908                makeFormulasMenu(menu);
2909            } else {
2910                super.handleMenuSelected(id, menu, idvWindow);
2911            }
2912        }
2913    
2914        private boolean didTabs = false;
2915        private boolean didNewWindow = false;
2916    
2917        public void makeDefaultLayoutMenu(final JMenu menu) {
2918            if (menu == null)
2919                throw new NullPointerException("Must provide a non-null default layout menu");
2920    
2921            menu.removeAll();
2922            JMenuItem saveLayout = new JMenuItem("Save");
2923                    McVGuiUtils.setMenuImage(saveLayout, Constants.ICON_DEFAULTLAYOUTADD_SMALL);
2924            saveLayout.setToolTipText("Save as default layout");
2925            saveLayout.addActionListener(new ActionListener() {
2926                public void actionPerformed(final ActionEvent e) {
2927                    ((McIDASV)idv).doSaveAsDefaultLayout();
2928                }
2929            });
2930    
2931            JMenuItem removeLayout = new JMenuItem("Remove");
2932                    McVGuiUtils.setMenuImage(removeLayout, Constants.ICON_DEFAULTLAYOUTDELETE_SMALL);
2933            removeLayout.setToolTipText("Remove saved default layout");
2934            removeLayout.addActionListener(new ActionListener() {
2935                public void actionPerformed(final ActionEvent e) {
2936                    idv.doClearDefaults();
2937                }
2938            });
2939    
2940            removeLayout.setEnabled(((McIDASV)idv).hasDefaultLayout());
2941    
2942            menu.add(saveLayout);
2943            menu.add(removeLayout);
2944        }
2945    
2946        /**
2947         * Bundles any compatible {@link ViewManager} states into {@link JMenuItem}s
2948         * and adds said {@code JMenuItem}s to {@code menu}. Incompatible states are
2949         * ignored.
2950         * 
2951         * <p>Each {@code JMenuItem} (except those under the {@literal "Delete"} menu--apologies)
2952         * associates a {@literal "view state"} and an {@link ObjectListener}. 
2953         * The {@code ObjectListener} uses this associated view state to attempt reinitialization
2954         * of {@code vm}.
2955         * 
2956         * <p>Override reasoning:
2957         * <ul>
2958         *   <li>terminology ({@literal "views"} rather than {@literal "viewpoints"}).</li>
2959         *   <li>
2960         *     use of {@link #filterVMMStatesWithVM(ViewManager, Collection)} to
2961         *     properly detect the {@literal "no saved views"} case.
2962         *   </li>
2963         * </ul>
2964         * 
2965         * @param menu Menu to populate. Should not be {@code null}.
2966         * @param vm {@code ViewManager} that might get reinitialized. Should not be {@code null}. 
2967         * 
2968         * @see ViewManager#initWith(ViewManager, boolean)
2969         * @see ViewManager#initWith(ViewState)
2970         * @see IdvUIManager#makeViewStateMenu(JMenu, ViewManager)
2971         */
2972        @Override public void makeViewStateMenu(final JMenu menu, final ViewManager vm) {
2973            List<TwoFacedObject> vmStates = filterVMMStatesWithVM(vm, getVMManager().getVMState());
2974            if (vmStates.isEmpty()) {
2975                JMenuItem item = new JMenuItem(Msg.msg("No Saved Views"));
2976                item.setEnabled(false);
2977                menu.add(item);
2978            } else {
2979                JMenu deleteMenu = new JMenu("Delete");
2980                makeDeleteViewsMenu(deleteMenu);
2981                menu.add(deleteMenu);
2982            }
2983    
2984            for (TwoFacedObject tfo : vmStates) {
2985              JMenuItem mi = new JMenuItem(tfo.getLabel().toString());
2986              menu.add(mi);
2987              mi.addActionListener(new ObjectListener(tfo.getId()) {
2988                  public void actionPerformed(final ActionEvent e) {
2989                      if (vm == null)
2990                          return;
2991    
2992                      if (theObject instanceof ViewManager) {
2993                          vm.initWith((ViewManager)theObject, true);
2994                      } else if (theObject instanceof ViewState) {
2995                          try {
2996                              vm.initWith((ViewState)theObject);
2997                          } catch (Throwable ex) {
2998                              logException("Initializing view with ViewState", ex);
2999                          }
3000                      } else {
3001                          LogUtil.consoleMessage("UIManager.makeViewStateMenu: Object of unknown type: "+theObject.getClass().getName());
3002                      }
3003                  }
3004              });
3005          }
3006        }
3007    
3008        /**
3009         * Returns a list of {@link TwoFacedObject}s that are known to be 
3010         * compatible with {@code vm}.
3011         * 
3012         * <p>This method is currently capable of dealing with {@code TwoFacedObject}s and
3013         * {@link ViewState}s within {@code states}. Any other types are ignored.
3014         * 
3015         * @param vm {@link ViewManager} to use for compatibility tests. {@code null} is allowed.
3016         * @param states Collection of objects to test against {@code vm}. {@code null} is allowed.
3017         * 
3018         * @return Either a {@link List} of compatible {@literal "view states"} or an empty {@code List}.
3019         * 
3020         * @see ViewManager#isCompatibleWith(ViewManager)
3021         * @see ViewManager#isCompatibleWith(ViewState)
3022         * @see #makeViewStateMenu(JMenu, ViewManager)
3023         */
3024        public static List<TwoFacedObject> filterVMMStatesWithVM(final ViewManager vm, final Collection<?> states) {
3025            if (vm == null || states == null || states.isEmpty())
3026                return Collections.emptyList();
3027    
3028            List<TwoFacedObject> validStates = new ArrayList<TwoFacedObject>(states.size());
3029            for (Object obj : states) {
3030                TwoFacedObject tfo = null;
3031                if (obj instanceof TwoFacedObject) {
3032                    tfo = (TwoFacedObject)obj;
3033                    if (vm.isCompatibleWith((ViewManager)tfo.getId())) {
3034                        continue;
3035                    }
3036                } else if (obj instanceof ViewState) {
3037                    if (!vm.isCompatibleWith((ViewState)obj)) {
3038                        continue;
3039                    }
3040                    tfo = new TwoFacedObject(((ViewState)obj).getName(), obj);
3041                } else {
3042                    LogUtil.consoleMessage("UIManager.filterVMMStatesWithVM: Object of unknown type: "+obj.getClass().getName());
3043                    continue;
3044                }
3045                validStates.add(tfo);
3046            }
3047            return validStates;
3048        }
3049    
3050        /**
3051         * Overridden to build a custom Display menu.
3052         * @see ucar.unidata.idv.ui.IdvUIManager#initializeDisplayMenu(JMenu)
3053         */
3054        @Override
3055        protected void initializeDisplayMenu(JMenu displayMenu) {
3056            JMenu m;
3057            JMenuItem mi;
3058    
3059            // Get the list of possible standalone control descriptors
3060            Hashtable controlsHash = new Hashtable();
3061            List controlDescriptors = getStandAloneControlDescriptors();
3062            for (int i = 0; i < controlDescriptors.size(); i++) {
3063                ControlDescriptor cd = (ControlDescriptor)controlDescriptors.get(i);
3064                String cdLabel = cd.getLabel();
3065                if (cdLabel.equals("Range Rings"))
3066                    controlsHash.put(cdLabel, cd);
3067                else if (cdLabel.equals("Range and Bearing"))
3068                    controlsHash.put(cdLabel, cd);
3069                else if (cdLabel.equals("Location Indicator"))
3070                    controlsHash.put(cdLabel, cd);
3071                else if (cdLabel.equals("Drawing Control"))
3072                    controlsHash.put(cdLabel, cd);
3073                else if (cdLabel.equals("Transect Drawing Control"))
3074                    controlsHash.put(cdLabel, cd);
3075            }
3076            
3077            // Build the menu
3078            ControlDescriptor cd;
3079            
3080            mi = new JMenuItem("Create Layer from Data Source...");
3081            mi.addActionListener(new ActionListener() {
3082                public void actionPerformed(ActionEvent ae) {
3083                    showDashboard("Data Sources");
3084                }
3085            });
3086            displayMenu.add(mi);
3087            
3088            mi = new JMenuItem("Layer Controls...");
3089            mi.addActionListener(new ActionListener() {
3090                public void actionPerformed(ActionEvent ae) {
3091                    showDashboard("Layer Controls");
3092                }
3093            });
3094            displayMenu.add(mi);
3095            
3096            displayMenu.addSeparator();
3097            
3098            cd = (ControlDescriptor)controlsHash.get("Range Rings");
3099            mi = makeControlDescriptorItem(cd);
3100            mi.setText("Add Range Rings");
3101            displayMenu.add(mi);
3102            
3103            cd = (ControlDescriptor)controlsHash.get("Range and Bearing");
3104            mi = makeControlDescriptorItem(cd);
3105            McVGuiUtils.setMenuImage(mi, Constants.ICON_RANGEANDBEARING_SMALL);
3106            mi.setText("Add Range and Bearing");
3107            displayMenu.add(mi);
3108            
3109            displayMenu.addSeparator();
3110            
3111            cd = (ControlDescriptor)controlsHash.get("Transect Drawing Control");
3112            mi = makeControlDescriptorItem(cd);
3113            mi.setText("Draw Transect...");
3114            displayMenu.add(mi);
3115            
3116            cd = (ControlDescriptor)controlsHash.get("Drawing Control");
3117            mi = makeControlDescriptorItem(cd);
3118            mi.setText("Draw Freely...");
3119            displayMenu.add(mi);
3120            
3121            displayMenu.addSeparator();
3122            
3123            cd = (ControlDescriptor)controlsHash.get("Location Indicator");
3124            mi = makeControlDescriptorItem(cd);
3125            McVGuiUtils.setMenuImage(mi, Constants.ICON_LOCATION_SMALL);
3126            mi.setText("Add Location Indicator");
3127            displayMenu.add(mi);
3128            
3129            ControlDescriptor locationDescriptor =
3130                    idv.getControlDescriptor("locationcontrol");
3131            if (locationDescriptor != null) {
3132                    List stations = idv.getLocationList();
3133                    ObjectListener listener = new ObjectListener(locationDescriptor) {
3134                            public void actionPerformed(ActionEvent ae, Object obj) {
3135                                    addStationDisplay((NamedStationTable) obj, (ControlDescriptor) theObject);
3136                            }
3137                    };
3138                    List menuItems = NamedStationTable.makeMenuItems(stations, listener);
3139                    displayMenu.add(GuiUtils.makeMenu("Plot Location Labels", menuItems));
3140            }
3141            
3142            displayMenu.addSeparator();
3143            
3144            mi = new JMenuItem("Add Background Image");
3145            McVGuiUtils.setMenuImage(mi, Constants.ICON_BACKGROUND_SMALL);
3146            mi.addActionListener(new ActionListener() {
3147                public void actionPerformed(ActionEvent ae) {
3148                    getIdv().doMakeBackgroundImage();
3149                }
3150            });
3151            displayMenu.add(mi);
3152            
3153            mi = new JMenuItem("Reset Map Layer to Defaults");
3154            mi.addActionListener(new ActionListener() {
3155                public void actionPerformed(ActionEvent ae) {
3156                    // TODO: Call IdvUIManager.addDefaultMap()... should be made private
3157    //                addDefaultMap();
3158                    ControlDescriptor mapDescriptor =
3159                        idv.getControlDescriptor("mapdisplay");
3160                    if (mapDescriptor == null) {
3161                        return;
3162                    }
3163                    String attrs =
3164                        "initializeAsDefault=true;displayName=Default Background Maps;";
3165                    idv.doMakeControl(new ArrayList(), mapDescriptor, attrs, null);
3166                }
3167            });
3168            displayMenu.add(mi);
3169            
3170            Msg.translateTree(displayMenu);
3171        }
3172    
3173        /**
3174         * Get the window title from the skin
3175         *
3176         * @param index  the skin index
3177         *
3178         * @return  the title
3179         */
3180        private String getWindowTitleFromSkin(final int index) {
3181            if (!skinToTitle.containsKey(index)) {
3182                IdvResourceManager mngr = getResourceManager();
3183                XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN);
3184                List<String> names = StringUtil.split(skins.getShortName(index), ">", true, true);
3185                String title = getStateManager().getTitle();
3186                if (names.size() > 0)
3187                    title = title + " - " + StringUtil.join(" - ", names);
3188                skinToTitle.put(index, title);
3189            }
3190            return skinToTitle.get(index);
3191        }
3192    
3193        @SuppressWarnings("unchecked")
3194        @Override public Hashtable getMenuIds() {
3195            return menuIds;
3196        }
3197    
3198        @SuppressWarnings("unchecked")
3199        @Override public JMenuBar doMakeMenuBar(final IdvWindow idvWindow) {
3200            Hashtable<String, JMenuItem> menuMap = new Hashtable<String, JMenuItem>();
3201            JMenuBar menuBar = new JMenuBar();
3202            final IdvResourceManager mngr = getResourceManager();
3203            XmlResourceCollection xrc = mngr.getXmlResources(mngr.RSC_MENUBAR);
3204            Hashtable<String, ImageIcon> actionIcons = new Hashtable<String, ImageIcon>();
3205    
3206            for (int i = 0; i < xrc.size(); i++)
3207                GuiUtils.processXmlMenuBar(xrc.getRoot(i), menuBar, getIdv(), menuMap, actionIcons);
3208    
3209            menuIds = new Hashtable<String, JMenuItem>(menuMap);
3210    
3211            // Ensure that the "help" menu is the last menu.
3212            JMenuItem helpMenu = menuMap.get(MENU_HELP);
3213            if (helpMenu != null) {
3214                menuBar.remove(helpMenu);
3215                menuBar.add(helpMenu);
3216            }
3217    
3218            //TODO: Perhaps we will put the different skins in the menu?
3219            JMenu newDisplayMenu = (JMenu)menuMap.get(MENU_NEWDISPLAY);
3220            if (newDisplayMenu != null)
3221                GuiUtils.makeMenu(newDisplayMenu, makeSkinMenuItems(makeMenuBarActionListener(), true, false));
3222    
3223    //        final JMenu publishMenu = menuMap.get(MENU_PUBLISH);
3224    //        if (publishMenu != null) {
3225    //            if (!getPublishManager().isPublishingEnabled())
3226    //                publishMenu.getParent().remove(publishMenu);
3227    //            else
3228    //                getPublishManager().initMenu(publishMenu);
3229    //        }
3230    
3231            for (Entry<String, JMenuItem> e : menuMap.entrySet()) {
3232                if (!(e.getValue() instanceof JMenu))
3233                    continue;
3234                String menuId = e.getKey();
3235                JMenu menu = (JMenu)e.getValue();
3236                menu.addMenuListener(makeMenuBarListener(menuId, menu, idvWindow));
3237            }
3238            return menuBar;
3239        }
3240    
3241        private final ActionListener makeMenuBarActionListener() {
3242            final IdvResourceManager mngr = getResourceManager();
3243            return new ActionListener() {
3244                public void actionPerformed(final ActionEvent ae) {
3245                    XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN);
3246                    int skinIndex = ((Integer)ae.getSource()).intValue();
3247                    createNewWindow(null, true, getWindowTitleFromSkin(skinIndex),
3248                        skins.get(skinIndex).toString(), 
3249                        skins.getRoot(skinIndex, false), true, null);
3250                }
3251            };
3252        }
3253    
3254        private final MenuListener makeMenuBarListener(final String id, final JMenu menu, final IdvWindow idvWindow) {
3255            return new MenuListener() {
3256                public void menuCanceled(final MenuEvent e) { }
3257                public void menuDeselected(final MenuEvent e) { handleMenuDeSelected(id, menu, idvWindow); }
3258                public void menuSelected(final MenuEvent e) { handleMenuSelected(id, menu, idvWindow); }
3259            };
3260        }
3261    
3262        /**
3263         * Handle mouse clicks that occur within the toolbar.
3264         */
3265        private class PopupListener extends MouseAdapter {
3266    
3267            private JPopupMenu popup;
3268    
3269            public PopupListener(JPopupMenu p) {
3270                popup = p;
3271            }
3272    
3273            // handle right clicks on os x and linux
3274            public void mousePressed(MouseEvent e) {
3275                if (e.isPopupTrigger() == true)
3276                    popup.show(e.getComponent(), e.getX(), e.getY());
3277            }
3278    
3279            // Windows doesn't seem to trigger mousePressed() for right clicks, but
3280            // never fear; mouseReleased() does the job.
3281            public void mouseReleased(MouseEvent e) {
3282                if (e.isPopupTrigger() == true)
3283                    popup.show(e.getComponent(), e.getX(), e.getY());
3284            }
3285        }
3286    
3287        /**
3288         * Handle (polymorphically) the {@link ucar.unidata.idv.ui.DataControlDialog}.
3289         * This dialog is used to either select a display control to create
3290         * or is used to set the timers used for a {@link ucar.unidata.data.DataSource}.
3291         *
3292         * @param dcd The dialog
3293         */
3294        public void processDialog(DataControlDialog dcd) {
3295            int estimatedMB = getEstimatedMegabytes(dcd);
3296            
3297            if (estimatedMB > 0) {
3298                double totalMem = Runtime.getRuntime().maxMemory();
3299                double highMem = Runtime.getRuntime().totalMemory();
3300                double freeMem = Runtime.getRuntime().freeMemory();
3301                double usedMem = (highMem - freeMem);
3302                int availableMB = Math.round( ((float)totalMem - (float)usedMem) / 1024f / 1024f);
3303                int percentOfAvailable = Math.round((float)estimatedMB / (float)availableMB * 100f);
3304                
3305                if (percentOfAvailable > 95) {
3306                    String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>";
3307                    message += "which exceeds 95% of total amount available (" + availableMB +"MB).<br>";
3308                    message += "Data load cancelled.</html>";
3309                    JComponent msgLabel = new JLabel(message);
3310                            GuiUtils.showDialog("Data Size", msgLabel);
3311                            return;
3312                }
3313                else if (percentOfAvailable >= 75) {
3314                    String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>";
3315                    message += percentOfAvailable + "% of the total amount available (" + availableMB + "MB).<br>";
3316                    message += "Continue loading data?</html>";
3317                    JComponent msgLabel = new JLabel(message);
3318                            if (!GuiUtils.askOkCancel("Data Size", msgLabel)) {
3319                                    return;
3320                            }
3321                }
3322            }
3323            
3324            super.processDialog(dcd);
3325        }
3326    
3327        /**
3328         * Estimate the number of megabytes that will be used by this data selection
3329         */
3330        protected int getEstimatedMegabytes(DataControlDialog dcd) {
3331            int estimatedMB = 0;
3332            DataChoice dataChoice = dcd.getDataChoice();
3333            if (dataChoice != null) {
3334                Object[] selectedControls = dcd.getSelectedControls();
3335                for (int i = 0; i < selectedControls.length; i++) {
3336                    ControlDescriptor cd = (ControlDescriptor) selectedControls[i];
3337    
3338                    //Check if the data selection is ok
3339                    if(!dcd.getDataSelectionWidget().okToCreateTheDisplay(cd.doesLevels())) {
3340                        continue;
3341                    }
3342    
3343                    DataSelection dataSelection = dcd.getDataSelectionWidget().createDataSelection(cd.doesLevels());
3344                                    
3345                    // Get the size in pixels of the requested image
3346                    Object gotSize = dataSelection.getProperty("SIZE");
3347                    if (gotSize == null) {
3348                            continue;
3349                    }
3350                    List<String> dims = StringUtil.split((String)gotSize, " ", false, false);
3351                    int myLines = -1;
3352                    int myElements = -1;
3353                    if (dims.size() == 2) {
3354                            try {
3355                                    myLines = Integer.parseInt(dims.get(0));
3356                                    myElements = Integer.parseInt(dims.get(1));
3357                            }
3358                            catch (Exception e) { }
3359                    }
3360    
3361                    // Get the count of times requested
3362                    int timeCount = 1;
3363                    DataSelectionWidget dsw = dcd.getDataSelectionWidget();
3364                    List times = dsw.getSelectedDateTimes();
3365                    List timesAll = dsw.getAllDateTimes();
3366                    if (times != null && times.size() > 0) {
3367                            timeCount = times.size();
3368                    }
3369                    else if (timesAll != null && timesAll.size() > 0) {
3370                            timeCount = timesAll.size();
3371                    }
3372                    
3373                    // Total number of pixels
3374                    // Assumed lines x elements x times x 4bytes
3375                    // Empirically seems to be taking *twice* that (64bit fields??)
3376                    float totalPixels = (float)myLines * (float)myElements * (float)timeCount;
3377                    float totalBytes = totalPixels * 4 * 2;
3378                    estimatedMB += Math.round(totalBytes / 1024f / 1024f);
3379                                    
3380                    // Add amount taken by textures... guess at 2048x2048 base size (possibly not always correct)
3381                    // Approx 16mb per large texture
3382                    int textureDimensions = 2048;
3383                    int mbPerTexture = Math.round((float)textureDimensions * (float)textureDimensions * 4 / 1024f / 1024f);
3384                    int textureCount = (int)Math.ceil((float)myLines / 2048f) * (int)Math.ceil((float)myElements / 2048f);
3385                    int additionalMB = textureCount * mbPerTexture * timeCount;
3386                    
3387                    estimatedMB += additionalMB;
3388                }
3389            }
3390            
3391            return estimatedMB;
3392        }
3393    
3394        /**
3395         * Represents a SavedBundle as a tree.
3396         */
3397        private class BundleTreeNode {
3398    
3399            private String name;
3400    
3401            private SavedBundle bundle;
3402    
3403            private List<BundleTreeNode> kids;
3404    
3405            /**
3406             * This constructor is used to build a node that is considered a
3407             * "parent." These nodes only have child nodes, no SavedBundles. This
3408             * was done so that distinguishing between bundles and bundle
3409             * subcategories would be easy.
3410             * 
3411             * @param name The name of this node. For a parent node with
3412             *        "Toolbar>cat" as the path, the name parameter would contain
3413             *        only "cat."
3414             */
3415            public BundleTreeNode(String name) {
3416                this(name, null);
3417            }
3418    
3419            /**
3420             * Nodes constructed using this constructor can only ever be child
3421             * nodes.
3422             * 
3423             * @param name The name of the SavedBundle.
3424             * @param bundle A reference to the SavedBundle.
3425             */
3426            public BundleTreeNode(String name, SavedBundle bundle) {
3427                this.name = name;
3428                this.bundle = bundle;
3429                kids = new LinkedList<BundleTreeNode>();
3430            }
3431    
3432            /**
3433             * @param child The node to be added to the current node.
3434             */
3435            public void addChild(BundleTreeNode child) {
3436                kids.add(child);
3437            }
3438    
3439            /**
3440             * @return Returns all child nodes of this node.
3441             */
3442            public List<BundleTreeNode> getChildren() {
3443                return kids;
3444            }
3445    
3446            /**
3447             * @return Return the SavedBundle associated with this node (if any).
3448             */
3449            public SavedBundle getBundle() {
3450                return bundle;
3451            }
3452    
3453            /**
3454             * @return The name of this node.
3455             */
3456            public String getName() {
3457                return name;
3458            }
3459        }
3460    
3461        /**
3462         * <p>
3463         * A type of <code>HttpFormEntry</code> that supports line wrapping for 
3464         * text area entries.
3465         * </p>
3466         * 
3467         * @see HttpFormEntry
3468         */
3469        private static class FormEntry extends HttpFormEntry {
3470            /** Initial contents of this entry. */
3471            private String value = "";
3472    
3473            /** Whether or not the JTextArea should wrap lines. */
3474            private boolean wrap = true;
3475    
3476            /** Entry type. Used to remain compatible with the IDV. */
3477            private int type = HttpFormEntry.TYPE_AREA;
3478    
3479            /** Number of rows in the JTextArea. */
3480            private int rows = 5;
3481    
3482            /** Number of columns in the JTextArea. */
3483            private int cols = 30;
3484    
3485            /** GUI representation of this entry. */
3486            private JTextArea component = new JTextArea(value, rows, cols);
3487    
3488            /**
3489             * Required to keep Java happy.
3490             */
3491            public FormEntry() {
3492                super(HttpFormEntry.TYPE_AREA, "form_data[description]", 
3493                    "Description:");
3494            }
3495    
3496            /**
3497             * <p>
3498             * Using this constructor allows McIDAS-V to control whether or not a
3499             * HttpFormEntry performs line wrapping for JTextArea components.
3500             * </p>
3501             * 
3502             * @see HttpFormEntry#HttpFormEntry(int, String, String, String, int, int, boolean)
3503             */
3504            public FormEntry(boolean wrap, int type, String name, String label, String value, int rows, int cols, boolean required) {
3505                super(type, name, label, value, rows, cols, required);
3506                this.type = type;
3507                this.rows = rows;
3508                this.cols = cols;
3509                this.wrap = wrap;
3510            }
3511    
3512            /**
3513             * <p>
3514             * Overrides the IDV method so that the McIDAS-V support request form
3515             * will wrap lines in the "Description" field.
3516             * </p>
3517             * 
3518             * @see HttpFormEntry#addToGui(List)
3519             */
3520            @SuppressWarnings("unchecked")
3521            @Override public void addToGui(List guiComps) {
3522                if (type == HttpFormEntry.TYPE_AREA) {
3523                    guiComps.add(GuiUtils.top(GuiUtils.rLabel(getLabel())));
3524                    component.setLineWrap(wrap);
3525                    component.setWrapStyleWord(wrap);
3526                    JScrollPane sp = new JScrollPane(component);
3527                    sp.setPreferredSize(new Dimension(500, 200));
3528                    sp.setMinimumSize(new Dimension(500, 200));
3529                    guiComps.add(sp);
3530                } else {
3531                    super.addToGui(guiComps);
3532                }
3533            }
3534    
3535            /**
3536             * <p>
3537             * Since the IDV doesn't provide a getComponent for 
3538             * <code>addToGui</code>, we must make our <code>component</code> field
3539             * local to this class. 
3540             * Hijacks any value requests so that the local <code>component</code>
3541             * field is queried, not the IDV's.
3542             * </p>
3543             * 
3544             * @see HttpFormEntry#getValue()
3545             */
3546            @Override public String getValue() {
3547                if (type != HttpFormEntry.TYPE_AREA)
3548                    return super.getValue();
3549                return component.getText();
3550            }
3551    
3552            /**
3553             * <p>
3554             * Hijacks any requests to set the <code>component</code> field's text.
3555             * </p>
3556             * 
3557             * @see HttpFormEntry#setValue(String)
3558             */
3559            @Override public void setValue(final String newValue) {
3560                if (type == HttpFormEntry.TYPE_AREA)
3561                    component.setText(newValue);
3562                else
3563                    super.setValue(newValue);
3564            }
3565        }
3566    
3567        /**
3568         * A {@code ToolbarStyle} is a representation of the way icons associated
3569         * with current toolbar actions should be displayed. This notion is so far
3570         * limited to the sizing of icons, but that may change.
3571         */
3572        public enum ToolbarStyle {
3573            /**
3574             * Represents the current toolbar actions as large icons. Currently,
3575             * {@literal "large"} is defined as {@code 32 x 32} pixels.
3576             */
3577            LARGE("Large Icons", "action.icons.large", 32),
3578    
3579            /**
3580             * Represents the current toolbar actions as medium icons. Currently,
3581             * {@literal "medium"} is defined as {@code 22 x 22} pixels.
3582             */
3583            MEDIUM("Medium Icons", "action.icons.medium", 22),
3584    
3585            /** 
3586             * Represents the current toolbar actions as small icons. Currently,
3587             * {@literal "small"} is defined as {@code 16 x 16} pixels. 
3588             */
3589            SMALL("Small Icons", "action.icons.small", 16);
3590    
3591            /** Label to use in the toolbar customization popup menu. */
3592            private final String label;
3593    
3594            /** Signals that the user selected a specific icon size. */
3595            private final String action;
3596    
3597            /** Icon dimensions. Each icon should be {@code size * size}. */
3598            private final int size;
3599    
3600            /**
3601             * {@link #size} in {@link String} form, merely for use with the IDV's
3602             * preference functionality.
3603             */
3604            private final String sizeAsString;
3605    
3606            /**
3607             * Initializes a toolbar style.
3608             * 
3609             * @param label Label used in the toolbar popup menu.
3610             * @param action Command that signals the user selected this toolbar 
3611             * style.
3612             * @param size Dimensions of the icons.
3613             * 
3614             * @throws NullPointerException if {@code label} or {@code action} are
3615             * null.
3616             * 
3617             * @throws IllegalArgumentException if {@code size} is not positive.
3618             */
3619            ToolbarStyle(final String label, final String action, final int size) {
3620                if (label == null)
3621                    throw new NullPointerException("Label cannot be null");
3622                if (action == null)
3623                    throw new NullPointerException("Action cannot be null");
3624                if (size <= 0)
3625                    throw new IllegalArgumentException("Size must be a positive integer");
3626    
3627                this.label = label;
3628                this.action = action;
3629                this.size = size;
3630                this.sizeAsString = Integer.toString(size);
3631            }
3632    
3633            /**
3634             * Returns the label to use as a brief description of this style.
3635             */
3636            public String getLabel() {
3637                return label;
3638            }
3639    
3640            /**
3641             * Returns the action command associated with this style.
3642             */
3643            public String getAction() {
3644                return action;
3645            }
3646    
3647            /**
3648             * Returns the dimensions of icons used in this style.
3649             */
3650            public int getSize() {
3651                return size;
3652            }
3653    
3654            /**
3655             * Returns {@link #size} as a {@link String} to make cooperating with
3656             * the IDV preferences code easier.
3657             */
3658            public String getSizeAsString() {
3659                return sizeAsString;
3660            }
3661    
3662            /**
3663             * Returns a brief description of this ToolbarStyle. A typical 
3664             * example:<br/>
3665             * {@code [ToolbarStyle@1337: label="Large Icons", size=32]}
3666             * 
3667             * <p>Note that the format and details provided are subject to change.
3668             */
3669            public String toString() {
3670                return String.format("[ToolbarStyle@%x: label=%s, size=%d]", 
3671                    hashCode(), label, size);
3672            }
3673    
3674            /**
3675             * Convenience method for build the toolbar customization popup menu.
3676             * 
3677             * @param manager {@link UIManager} that will be listening for action
3678             * commands.
3679             * 
3680             * @return Menu item that has {@code manager} listening for 
3681             * {@link #action}.
3682             */
3683            protected JMenuItem buildMenuItem(final UIManager manager) {
3684                JMenuItem item = new JRadioButtonMenuItem(label);
3685                item.setActionCommand(action);
3686                item.addActionListener(manager);
3687                return item;
3688            }
3689        }
3690    
3691        /**
3692         * Represents what McIDAS-V {@literal "knows"} about IDV actions.
3693         */
3694        protected enum ActionAttribute {
3695    
3696            /**
3697             * Unique identifier for an IDV action. Required attribute.
3698             * 
3699             * @see IdvUIManager#ATTR_ID
3700             */
3701            ID(ATTR_ID), 
3702    
3703            /**
3704             * Path to an icon for this action. Currently required. Note that 
3705             * McIDAS-V differs from the IDV in that actions must support different
3706             * icon sizes. This is implemented in McIDAS-V by simply having the value
3707             * of this path be a valid {@literal "format string"}, 
3708             * such as {@code image="/edu/wisc/ssec/mcidasv/resources/icons/toolbar/background-image%d.png"}
3709             * 
3710             * <p>The upshot is that this value <b>will not be a valid path in 
3711             * McIDAS-V</b>. Use either {@link IdvAction#getMenuIcon()} or 
3712             * {@link IdvAction#getIconForStyle(ToolbarStyle)}.
3713             * 
3714             * @see IdvUIManager#ATTR_IMAGE
3715             * @see IdvAction#getRawIconPath()
3716             * @see IdvAction#getMenuIcon()
3717             * @see IdvAction#getIconForStyle(ToolbarStyle)
3718             */
3719            ICON(ATTR_IMAGE), 
3720    
3721            /**
3722             * Brief description of a IDV action. Required attribute.
3723             * @see IdvUIManager#ATTR_DESCRIPTION
3724             */
3725            DESCRIPTION(ATTR_DESCRIPTION), 
3726    
3727            /**
3728             * Allows actions to be clustered into arbitrary groups. Currently 
3729             * optional; defaults to {@literal "General"}.
3730             * @see IdvUIManager#ATTR_GROUP
3731             */
3732            GROUP(ATTR_GROUP, "General"), 
3733    
3734            /**
3735             * Actual method call used to invoke a given IDV action. Required 
3736             * attribute.
3737             * @see IdvUIManager#ATTR_ACTION
3738             */
3739            ACTION(ATTR_ACTION);
3740    
3741            /**
3742             * A blank {@link String} if this is a required attribute, or a 
3743             * {@code String} value to use in case this attribute has not been 
3744             * specified by a given IDV action.
3745             */
3746            private final String defaultValue;
3747    
3748            /**
3749             * String representation of this attribute as used by the IDV.
3750             * @see #asIdvString()
3751             */
3752            private final String idvString;
3753    
3754            /** Whether or not this attribute is required. */
3755            private final boolean required;
3756    
3757            /**
3758             * Creates a constant that represents a required IDV action attribute.
3759             * 
3760             * @param idvString Corresponding IDV attribute {@link String}. Cannot be {@code null}.
3761             * 
3762             * @throws NullPointerException if {@code idvString} is {@code null}.
3763             */
3764            ActionAttribute(final String idvString) {
3765                Contract.notNull(idvString, "Cannot be associated with a null IDV action attribute String");
3766    
3767                this.idvString = idvString; 
3768                this.defaultValue = ""; 
3769                this.required = true; 
3770            }
3771    
3772            /**
3773             * Creates a constant that represents an optional IDV action attribute.
3774             * 
3775             * @param idvString Corresponding IDV attribute {@link String}. 
3776             * Cannot be {@code null}.
3777             * @param defValue Default value for actions that do not have this 
3778             * attribute. Cannot be {@code null} or an empty {@code String}.
3779             * 
3780             * @throws NullPointerException if either {@code idvString} or 
3781             * {@code defValue} is {@code null}.
3782             * @throws IllegalArgumentException if {@code defValue} is an empty 
3783             * {@code String}.
3784             * 
3785             */
3786            ActionAttribute(final String idvString, final String defValue) {
3787                Contract.notNull(idvString, "Cannot be associated with a null IDV action attribute String");
3788                Contract.notNull(defValue, "Optional action attribute \"%s\" requires a non-null default value", toString());
3789    
3790                Contract.checkArg(!defValue.equals(""), "Optional action attribute \"%s\" requires something more descriptive than an empty String", toString());
3791    
3792                this.idvString = idvString; 
3793                this.defaultValue = defValue; 
3794                this.required = (defaultValue.equals("")); 
3795            }
3796    
3797            /**
3798             * @return The {@link String} representation of this attribute, as is 
3799             * used by the IDV.
3800             * 
3801             * @see IdvUIManager#ATTR_ACTION
3802             * @see IdvUIManager#ATTR_DESCRIPTION
3803             * @see IdvUIManager#ATTR_GROUP
3804             * @see IdvUIManager#ATTR_ID
3805             * @see IdvUIManager#ATTR_IMAGE
3806             */
3807            public String asIdvString() { return idvString; }
3808    
3809            /**
3810             * @return {@literal "Default value"} for this attribute. 
3811             * Blank {@link String}s imply that the attribute is required (and 
3812             * thus lacks a true default value).
3813             */
3814            public String defaultValue() { return defaultValue; }
3815    
3816            /**
3817             * @return Whether or not this attribute is a required attribute for 
3818             * valid {@link IdvAction}s.
3819             */
3820            public boolean isRequired() { return required; }
3821        }
3822    
3823        /**
3824         * Represents the set of known {@link IdvAction}s in an idiom that can be
3825         * easily used by both the IDV and McIDAS-V.
3826         */
3827        // TODO(jon:101): use Sets instead of maps and whatnot
3828        // TODO(jon:103): create an invalid IdvAction
3829        public static final class IdvActions {
3830    
3831            /** Maps {@literal "id"} values to {@link IdvAction}s. */
3832            private final Map<String, IdvAction> idToAction = new ConcurrentHashMap<String, IdvAction>();
3833    
3834            /** Collects {@link IdvAction}s {@literal "under"} common group values. */
3835            // TODO(jon:102): this should probably become concurrency-friendly.
3836            private final Map<String, Set<IdvAction>> groupToActions = new LinkedHashMap<String, Set<IdvAction>>();
3837    
3838            /**
3839             * 
3840             * 
3841             * @param idv Reference to the IDV {@literal "god"} object. Cannot be {@code null}.
3842             * @param collectionId IDV resource collection that contains our actions. Cannot be {@code null}.
3843             * 
3844             * @throws NullPointerException if {@code idv} or {@code collectionId} 
3845             * is {@code null}. 
3846             */
3847            public IdvActions(final IntegratedDataViewer idv, final XmlIdvResource collectionId) {
3848                Contract.notNull(idv, "Cannot provide a null IDV reference");
3849                Contract.notNull(collectionId, "Cannot build actions from a null collection id");
3850    
3851                // i lub u xpath (but how much slower is this?)
3852                String query = "//action[@id and @image and @description and @action]";
3853                for (Element e : elements(idv, collectionId, query)) {
3854                    IdvAction a = new IdvAction(e);
3855                    String id = a.getAttribute(ActionAttribute.ID);
3856                    idToAction.put(id, a);
3857                    String group = a.getAttribute(ActionAttribute.GROUP);
3858                    if (!groupToActions.containsKey(group)) {
3859                        groupToActions.put(group, new LinkedHashSet<IdvAction>());
3860                    }
3861                    Set<IdvAction> groupedIds = groupToActions.get(group);
3862                    groupedIds.add(a);
3863                }
3864            }
3865    
3866            /**
3867             * Attempts to return the {@link IdvAction} associated with the given
3868             * {@code actionId}.
3869             * 
3870             * @param actionId Identifier to use in the search. Cannot be 
3871             * {@code null}.
3872             * 
3873             * @return Either the {@code IdvAction} that matches {@code actionId} 
3874             * or {@code null} if there was no match.
3875             * 
3876             * @throws NullPointerException if {@code actionId} is {@code null}.
3877             */
3878            // TODO(jon:103) here
3879            public IdvAction getAction(final String actionId) {
3880                Contract.notNull(actionId, "Null action identifiers are not allowed");
3881                return idToAction.get(actionId);
3882            }
3883    
3884            /**
3885             * Searches for the action associated with {@code actionId} and 
3886             * returns the value associated with the given {@link ActionAttribute}.
3887             * 
3888             * @param actionId Identifier to search for. Cannot be {@code null}.
3889             * @param attr Attribute whose value is desired. Cannot be {@code null}.
3890             * 
3891             * @return Either the desired attribute value of the desired action, 
3892             * or {@code null} if {@code actionId} has no associated action.
3893             * 
3894             * @throws NullPointerException if either {@code actionId} or 
3895             * {@code attr} is {@code null}.
3896             */
3897            // TODO(jon:103) here
3898            public String getAttributeForAction(final String actionId, final ActionAttribute attr) {
3899                Contract.notNull(actionId, "Null action identifiers are not allowed");
3900                Contract.notNull(attr, "Actions cannot have values associated with a null attribute");
3901                IdvAction action = idToAction.get(actionId);
3902                if (action == null) {
3903                    return null;
3904                }
3905                return action.getAttribute(attr);
3906            }
3907    
3908            /**
3909             * Attempts to return the XML {@link Element} that {@literal "represents"} the
3910             * action associated with {@code actionId}.
3911             * 
3912             * @param actionId Identifier whose XML element is desired. Cannot be {@code null}.
3913             * 
3914             * @return Either the XML element associated with {@code actionId} or {@code null}.
3915             * 
3916             * @throws NullPointerException if {@code actionId} is {@code null}.
3917             * 
3918             * @see IdvAction#originalElement
3919             */
3920            // TODO(jon:103) here
3921            public Element getElementForAction(final String actionId) {
3922                Contract.notNull(actionId, "Cannot search for a null action identifier");
3923                IdvAction action = idToAction.get(actionId);
3924                if (action == null) {
3925                    return null;
3926                }
3927                return action.getElement();
3928            }
3929    
3930            /**
3931             * Attempts to return an {@link Icon} for a given {@link ActionAttribute#ID} and
3932             * {@link ToolbarStyle}.
3933             * 
3934             * @param actionId ID of the action whose {@literal "styled"} icon is 
3935             * desired. Cannot be {@code null}.
3936             * @param style Desired {@code Icon} style. Cannot be {@code null}.
3937             * 
3938             * @return Either the {@code Icon} associated with {@code actionId} 
3939             * and {@code style}, or {@code null}.
3940             * 
3941             * @throws NullPointerException if either {@code actionId} or 
3942             * {@code style} is {@code null}.
3943             */
3944            // TODO(jon:103) here
3945            public Icon getStyledIconFor(final String actionId, final ToolbarStyle style) {
3946                Contract.notNull(actionId, "Cannot get an icon for a null action identifier");
3947                Contract.notNull(style, "Cannot get an icon for a null ToolbarStyle");
3948                IdvAction a = idToAction.get(actionId);
3949                if (a == null) {
3950                    return null;
3951                }
3952                return a.getIconForStyle(style);
3953            }
3954    
3955            // TODO(jon:105): replace with something better
3956            public List<String> getAttributes(final ActionAttribute attr) {
3957                Contract.notNull(attr, "Actions cannot have null attributes");
3958                List<String> attributeList = arrList();
3959                for (Map.Entry<String, IdvAction> entry : idToAction.entrySet()) {
3960                    attributeList.add(entry.getValue().getAttribute(attr));
3961                }
3962                return attributeList;
3963            }
3964    
3965            /**
3966             * @return List of all known {@code IdvAction}s.
3967             */
3968            public List<IdvAction> getAllActions() {
3969                return arrList(idToAction.values());
3970            }
3971    
3972            /**
3973             * @return List of all known action groupings.
3974             * 
3975             * @see ActionAttribute#GROUP
3976             * @see #getActionsForGroup(String)
3977             */
3978            public List<String> getAllGroups() {
3979                return arrList(groupToActions.keySet());
3980            }
3981    
3982            /**
3983             * Returns the {@link Set} of {@link IdvAction}s associated with the 
3984             * given {@code group}.
3985             * 
3986             * @param group Group whose associated actions you want. Cannot be 
3987             * {@code null}.
3988             * 
3989             * @return Collection of {@code IdvAction}s associated with 
3990             * {@code group}. A blank collection is returned if there are no actions
3991             * associated with {@code group}.
3992             * 
3993             * @throws NullPointerException if {@code group} is {@code null}.
3994             * 
3995             * @see ActionAttribute#GROUP
3996             * @see #getAllGroups()
3997             */
3998            public Set<IdvAction> getActionsForGroup(final String group) {
3999                Contract.notNull(group, "Actions cannot be associated with a null group");
4000                if (!groupToActions.containsKey(group)) {
4001                    return Collections.emptySet();
4002                }
4003                return groupToActions.get(group);
4004            }
4005    
4006            /**
4007             * Returns a summary of the known IDV actions. Please note that this 
4008             * format is subject to change, and is not intended for serialization.
4009             * 
4010             * @return String that looks like 
4011             * {@code [IdvActions@HASHCODE: actions=...]}.
4012             */
4013            @Override public String toString() {
4014                return String.format("[IdvActions@%x: actions=%s]", hashCode(), idToAction);
4015            }
4016        }
4017    
4018        /**
4019         * Represents an individual IDV action. Should be fairly adaptable to
4020         * unforeseen changes from Unidata?
4021         */
4022        // TODO(jon:106): Implement equals/hashCode so that you can use these in Sets. The only relevant value should be the id, right?
4023        public static final class IdvAction {
4024    
4025            /** The XML {@link Element} that represents this IDV action. */
4026            private final Element originalElement;
4027    
4028            /** Mapping of (known) XML attributes to values for this individual action. */
4029            private final Map<ActionAttribute, String> attributes;
4030    
4031            /** 
4032             * Simple {@literal "cache"} for the different icons this action has
4033             * displayed. This is {@literal "lazy"}, so the cache does not contain
4034             * icons for {@link ToolbarStyle}s that haven't been used. 
4035             */
4036            private final Map<ToolbarStyle, Icon> iconCache = new ConcurrentHashMap<ToolbarStyle, Icon>();
4037    
4038            /**
4039             * Creates a representation of an IDV action using a given {@link Element}.
4040             * 
4041             * @param element XML representation of an IDV action. Cannot be {@code null}.
4042             * 
4043             * @throws NullPointerException if {@code element} is {@code null}.
4044             * @throws IllegalArgumentException if {@code element} is not a valid IDV action.
4045             * 
4046             * @see UIManager#isValidIdvAction(Element)
4047             */
4048            public IdvAction(final Element element) {
4049                Contract.notNull(element, "Cannot build an action from a null element");
4050                // TODO(jon:107): need a way to diagnose what's wrong with the action?
4051                Contract.checkArg(isValidIdvAction(element), "Action lacks required attributes");
4052                originalElement = element;
4053                attributes = actionElementToMap(element);
4054            }
4055    
4056            /**
4057             * @return Returns the {@literal "raw"} path to the icon associated 
4058             * with this action. Remember that this is actually a {@literal "format string"}
4059             * and should not be considered a valid path! 
4060             * 
4061             * @see #getIconForStyle(ToolbarStyle)
4062             */
4063            public String getRawIconPath() {
4064                return attributes.get(ActionAttribute.ICON);
4065            }
4066    
4067            /**
4068             * @return Returns the {@link Icon} associated with {@link ToolbarStyle#SMALL}.
4069             */
4070            public Icon getMenuIcon() {
4071                return getIconForStyle(ToolbarStyle.SMALL);
4072            }
4073    
4074            /**
4075             * Returns the {@link Icon} associated with this action and the given
4076             * {@link ToolbarStyle}.
4077             * 
4078             * @param style {@literal "Style"} of the {@code Icon} to be returned.
4079             * Cannot be {@code null}.
4080             * 
4081             * @return This action's {@code Icon} with {@code style} {@literal "applied."}
4082             * 
4083             * @see ActionAttribute#ICON
4084             * @see #iconCache
4085             */
4086            public Icon getIconForStyle(final ToolbarStyle style) {
4087                Contract.notNull(style, "Cannot build an icon for a null ToolbarStyle");
4088    
4089                if (!iconCache.containsKey(style)) {
4090                    String styledPath = String.format(getRawIconPath(), style.getSize());
4091                    URL tmp = getClass().getResource(styledPath);
4092                    iconCache.put(style, new ImageIcon(Toolkit.getDefaultToolkit().getImage(tmp)));
4093                }
4094                return iconCache.get(style);
4095            }
4096    
4097            /**
4098             * @return Returns the identifier of this {@code IdvAction}.
4099             */
4100            public String getId() {
4101                return getAttribute(ActionAttribute.ID);
4102            }
4103    
4104            /**
4105             * Representation of this {@code IdvAction} as an {@literal "IDV action call"}.
4106             * 
4107             * @return String that is suitable to hand off to the IDV for execution. 
4108             */
4109            public String getCommand() {
4110                return "idv.handleAction('action:"+getAttribute(ActionAttribute.ID)+"')";
4111            }
4112    
4113            /**
4114             * Returns the value associated with a given {@link ActionAttribute} 
4115             * for this action.
4116             * 
4117             * @param attr ActionAttribute whose value you want. Cannot be {@code null}.
4118             * 
4119             * @return Value associated with {@code attr}.
4120             * 
4121             * @throws NullPointerException if {@code attr} is {@code null}.
4122             */
4123            public String getAttribute(final ActionAttribute attr) {
4124                Contract.notNull(attr, "No values can be associated with a null ActionAttribute");
4125                return attributes.get(attr);
4126            }
4127    
4128            /**
4129             * @return The XML {@link Element} used to create this {@code IdvAction}.
4130             */
4131            // TODO(jon:104): any way to copy this element? if so, this can become an immutable class!
4132            public Element getElement() {
4133                return originalElement;
4134            }
4135    
4136            /**
4137             * Returns a brief description of this action. Please note that the 
4138             * format is subject to change and is not intended for serialization.
4139             * 
4140             * @return String that looks like {@code [IdvAction@HASHCODE: attributes=...]}.
4141             */
4142            @Override public String toString() {
4143                return String.format("[IdvAction@%x: attributes=%s]", hashCode(), attributes);
4144            }
4145        }
4146    }