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