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