001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2025
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     * Get the list of  initial skins to create windows for
2569     *
2570     * @return List of UI skins to create
2571     */
2572    private List<String> getInitialSkins() {
2573        //Run the skins through the getResourcePath in case there are any %USERPATH% macros
2574        List<String> temp = StringUtil.split(
2575                getStateManager().getProperty("idv.ui.initskins", ""), ";", true,
2576                true);
2577        List<String> skins = new ArrayList<String>();
2578        for(String skin: temp) {
2579            skins.add(getResourceManager().getResourcePath(skin));
2580        }
2581        return skins;
2582    }
2583
2584    /**
2585     * Create the basic windows. This gets called at start up and if the user
2586     * presses "show dashboard" and there isn't any windows available
2587     */
2588    @Override public void doMakeBasicWindows() {
2589        splashMsg("Creating User Interface");
2590        List skins = getInitialSkins();
2591        for (int i = 0; i < skins.size(); i++) {
2592            String skin = (String) skins.get(i);
2593            try {
2594                createNewWindow(new ArrayList(), skin);
2595            } catch (Throwable exc) {
2596                logException("Creating UI from skin:" + skin, exc);
2597            }
2598        }
2599    }
2600
2601    /**
2602     * Method to do the work of showing the Data Explorer (nee Dashboard).
2603     * 
2604     * @param tabName Name of the tab that should be made active. 
2605     *                Cannot be {@code null}, but empty {@code String} values 
2606     *                will not change the active tab.
2607     */
2608    @SuppressWarnings("unchecked") // IdvWindow.getWindows only adds IdvWindows.
2609    public void showDashboard(String tabName) {
2610        if (!initDone) {
2611            return;
2612        } else if (dashboard == null) {
2613            showWaitCursor();
2614            doMakeBasicWindows();
2615            showNormalCursor();
2616            String title = makeTitle(getStateManager().getTitle(), Constants.DATASELECTOR_NAME);
2617            for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) {
2618                if (title.equals(window.getTitle())) {
2619                    dashboard = window;
2620                    dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
2621                }
2622            }
2623        } else {
2624            dashboard.show();
2625        }
2626
2627        if (tabName.isEmpty()) {
2628            return;
2629        }
2630
2631        // Dig two panels deep looking for a JTabbedPane
2632        // If you find one, try to show the requested tab name
2633        JComponent contents = dashboard.getContents();
2634        JComponent component = (JComponent)contents.getComponent(0);
2635        JTabbedPane tPane = null;
2636        if (component instanceof JTabbedPane) {
2637            tPane = (JTabbedPane)component;
2638        }
2639        else {
2640            JComponent component2 = (JComponent)component.getComponent(0);
2641            if (component2 instanceof JTabbedPane) {
2642                tPane = (JTabbedPane)component2;
2643            }
2644        }
2645        if (tPane != null) {
2646            for (int i=0; i<tPane.getTabCount(); i++) {
2647                if (tabName.equals(tPane.getTitleAt(i))) {
2648                    tPane.setSelectedIndex(i);
2649                    break;
2650                }
2651            }
2652        }
2653    }
2654
2655    /**
2656     * Search through list of {@link IdvWindow IdvWindows} and return the {@literal "Data Explorer"},
2657     * if it exists.
2658     *
2659     * @return {@literal "Data Explorer"} window or {@code null} if it does not exist.
2660     */
2661    public IdvWindow getDashboardWindow() {
2662        IdvWindow dash = null;
2663        List<IdvWindow> windows = cast(IdvWindow.getWindows());
2664        for (IdvWindow window : windows) {
2665            if (Constants.DATASELECTOR_NAME.equals(window.getTitle())) {
2666                dash = window;
2667                break;
2668            }
2669        }
2670        return dash;
2671    }
2672
2673    /**
2674     * Show the support request form
2675     *
2676     * @param description Default value for the description form entry
2677     * @param stackTrace The stack trace that caused this error.
2678     * @param dialog The dialog to put the gui in, if non-null.
2679     */
2680    public void showSupportForm(final String description, 
2681        final String stackTrace, final JDialog dialog) 
2682    {
2683        java.awt.EventQueue.invokeLater(() -> {
2684            // TODO: mcvstatecollector should have a way to gather the
2685            // exception information..
2686            McIDASV mcv = (McIDASV)getIdv();
2687            new SupportForm(getStore(), new McvStateCollector(mcv)).setVisible(true);
2688        });
2689    }
2690
2691    /**
2692     * Attempts to locate and display a dashboard component using an ID.
2693     * 
2694     * @param id ID of the desired component.
2695     * 
2696     * @return True if {@code id} corresponds to a component. False otherwise.
2697     */
2698    public boolean showDashboardComponent(String id) {
2699        Object comp = findComponent(id);
2700        if (comp != null) {
2701            GuiUtils.showComponentInTabs((JComponent)comp);
2702            return true;
2703        } else {
2704            super.showDashboard();
2705            for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) {
2706                String title = makeTitle(
2707                    getStateManager().getTitle(),
2708                    Constants.DATASELECTOR_NAME
2709                );
2710                if (title.equals(window.getTitle())) {
2711                    dashboard = window;
2712                    dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
2713                }
2714            }
2715        }
2716        return false;
2717    }
2718
2719    /**
2720     * Close and dispose of the splash window (if it has been created).
2721     */
2722    @Override
2723    public void splashClose() {
2724        if (splash != null) {
2725            splash.doClose();
2726        }
2727    }
2728
2729    /**
2730     * Show a message in the splash screen (if it exists)
2731     *
2732     * @param m The message to show
2733     */
2734    @Override public void splashMsg(String m) {
2735        if (splash != null) {
2736            splash.splashMsg(m);
2737        }
2738    }
2739
2740    /**
2741     * Uses a given toolbar editor to repopulate all toolbars so that they 
2742     * correspond to the user's choice of actions.
2743     * 
2744     * @param tbe The toolbar editor that contains the actions the user wants.
2745     */
2746    public void setCurrentToolbars(final McvToolbarEditor tbe) {
2747        List<TwoFacedObject> tfos = tbe.getTLP().getCurrentEntries();
2748        List<String> buttonIds = new ArrayList<>(tfos.size());
2749        for (TwoFacedObject tfo : tfos) {
2750            if (McvToolbarEditor.isSpace(tfo)) {
2751                buttonIds.add(null);
2752            } else {
2753                buttonIds.add(TwoFacedObject.getIdString(tfo));
2754            }
2755        }
2756
2757        cachedButtons = buttonIds;
2758
2759        for (JToolBar toolbar : toolbars) {
2760            toolbar.setVisible(false);
2761            populateToolbar(toolbar);
2762            toolbar.setVisible(true);
2763        }
2764    }
2765
2766    /**
2767     * Append a string and object to the buffer
2768     *
2769     * @param sb  StringBuffer to append to
2770     * @param name  Name of the object
2771     * @param value  the object value
2772     */
2773    private void append(StringBuffer sb, String name, Object value) {
2774        sb.append("<b>").append(name).append("</b>: ").append(value).append("<br>");
2775    }
2776
2777    private JMenuItem makeControlDescriptorItem(ControlDescriptor cd) {
2778        JMenuItem mi = new JMenuItem();
2779        if (cd != null) {
2780            mi = new JMenuItem(cd.getLabel());
2781            mi.addActionListener(new ObjectListener(cd) {
2782                public void actionPerformed(ActionEvent ev) {
2783                    idv.doMakeControl(new ArrayList(),
2784                        (ControlDescriptor)theObject);
2785                }
2786            });
2787        }
2788        return mi;
2789    }
2790
2791    /* (non-javadoc)
2792     * Overridden so that the toolbar will update upon saving a bundle.
2793     */
2794    @Override public void displayTemplatesChanged() {
2795        super.displayTemplatesChanged();
2796        for (JToolBar toolbar : toolbars) {
2797            toolbar.setVisible(false);
2798            populateToolbar(toolbar);
2799            toolbar.setVisible(true);
2800        }
2801    }
2802
2803    /**
2804     * Called when there has been any change to the favorite bundles and is
2805     * most useful for triggering an update to the {@literal "toolbar bundles"}.
2806     */
2807    @Override public void favoriteBundlesChanged() {
2808        SwingUtilities.invokeLater(() -> {
2809            for (JToolBar toolbar : toolbars) {
2810                toolbar.setVisible(false);
2811                populateToolbar(toolbar);
2812                toolbar.setVisible(true);
2813            }
2814        });
2815    }
2816
2817    /**
2818     * Show the support request form in a non-swing thread. We do this because we cannot
2819     * call the HttpFormEntry.showUI from a swing thread
2820     *
2821     * @param description Default value for the description form entry
2822     * @param stackTrace The stack trace that caused this error.
2823     * @param dialog The dialog to put the gui in, if non-null.
2824     */
2825
2826    private void showSupportFormInThread(String description,
2827                                         String stackTrace, JDialog dialog) {
2828        List<HttpFormEntry> entries = new ArrayList<>();
2829
2830        StringBuffer extra   = new StringBuffer("<h3>McIDAS-V</h3>\n");
2831        Hashtable<String, String> table = 
2832            ((StateManager)getStateManager()).getVersionInfo();
2833        append(extra, "mcv.version.general", table.get("mcv.version.general"));
2834        append(extra, "mcv.version.build", table.get("mcv.version.build"));
2835        append(extra, "idv.version.general", table.get("idv.version.general"));
2836        append(extra, "idv.version.build", table.get("idv.version.build"));
2837
2838        extra.append("<h3>OS</h3>\n");
2839        append(extra, "os.name", System.getProperty("os.name"));
2840        append(extra, "os.arch", System.getProperty("os.arch"));
2841        append(extra, "os.version", System.getProperty("os.version"));
2842
2843        extra.append("<h3>Java</h3>\n");
2844        append(extra, "java.vendor", System.getProperty("java.vendor"));
2845        append(extra, "java.version", System.getProperty("java.version"));
2846        append(extra, "java.home", System.getProperty("java.home"));
2847
2848        StringBuffer javaInfo = new StringBuffer();
2849        javaInfo.append("Java: home: " + System.getProperty("java.home"));
2850        javaInfo.append(" version: " + System.getProperty("java.version"));
2851
2852        Class c = null;
2853        try {
2854            c = Class.forName("javax.media.j3d.VirtualUniverse");
2855            Method method = Misc.findMethod(c, "getProperties",
2856                                            new Class[] {});
2857            if (method == null) {
2858                javaInfo.append("j3d <1.3");
2859            } else {
2860                try {
2861                    Map m = (Map)method.invoke(c, new Object[] {});
2862                    javaInfo.append(" j3d:" + m.get("j3d.version"));
2863                    append(extra, "j3d.version", m.get("j3d.version"));
2864                    append(extra, "j3d.vendor", m.get("j3d.vendor"));
2865                    append(extra, "j3d.renderer", m.get("j3d.renderer"));
2866                } catch (Exception exc) {
2867                    javaInfo.append(" j3d:" + "unknown");
2868                }
2869            }
2870        } catch (ClassNotFoundException exc) {
2871            append(extra, "j3d", "none");
2872        }
2873
2874        boolean persistCC = getStore().get("mcv.supportreq.cc", true);
2875
2876        JCheckBox ccMyself = new JCheckBox("Send Copy of Support Request to Me", persistCC);
2877        ccMyself.addActionListener(e -> {
2878            JCheckBox cb = (JCheckBox)e.getSource();
2879            getStore().put("mcv.supportreq.cc", cb.isSelected());
2880        });
2881
2882        boolean doWrap = idv.getProperty(PROP_WRAP_SUPPORT_DESC, true);
2883
2884        HttpFormEntry descriptionEntry;
2885        HttpFormEntry nameEntry;
2886        HttpFormEntry emailEntry;
2887        HttpFormEntry orgEntry;
2888
2889        entries.add(nameEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2890                "form_data[fromName]", "Name:",
2891                getStore().get(PROP_HELP_NAME, (String) null)));
2892        entries.add(emailEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2893                "form_data[email]", "Your Email:",
2894                getStore().get(PROP_HELP_EMAIL, (String) null)));
2895        entries.add(orgEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2896                "form_data[organization]", "Organization:",
2897                getStore().get(PROP_HELP_ORG, (String) null)));
2898        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2899                                      "form_data[subject]", "Subject:"));
2900
2901        entries.add(
2902            new HttpFormEntry(
2903                HttpFormEntry.TYPE_LABEL, "",
2904                "<html>Please provide a <i>thorough</i> description of the problem you encountered:</html>"));
2905        entries.add(descriptionEntry =
2906            new FormEntry(doWrap, HttpFormEntry.TYPE_AREA,
2907                              "form_data[description]", "Description:",
2908                              description, 5, 30, true));
2909
2910        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE,
2911                                      "form_data[att_two]", "Attachment 1:", "",
2912                                      false));
2913        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE,
2914                                      "form_data[att_three]", "Attachment 2:", "",
2915                                      false));
2916
2917        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2918                                      "form_data[submit]", "", "Send Email"));
2919        
2920        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2921                                      "form_data[p_version]", "",
2922                                      getStateManager().getVersion()
2923                                      + " build date:"
2924                                      + getStateManager().getBuildDate()));
2925        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2926                                      "form_data[opsys]", "",
2927                                      System.getProperty("os.name")));
2928        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2929                                      "form_data[hardware]", "",
2930                                      javaInfo.toString()));
2931        
2932        JLabel topLabel = 
2933            new JLabel("<html>This form allows you to send a support request to the McIDAS Help Desk.<br></html>");
2934
2935        JCheckBox includeBundleCbx =
2936            new JCheckBox("Include Current State as Bundle", false);
2937
2938        List<JCheckBox> checkboxes = list(includeBundleCbx, ccMyself);
2939
2940        boolean alreadyHaveDialog = true;
2941        if (dialog == null) {
2942            // NOTE: if the dialog is modeless you can leave alreadyHaveDialog
2943            // alone. If the dialog is modal you need to set alreadyHaveDialog
2944            // to false.
2945            // If alreadyHaveDialog is false with a modeless dialog, the later
2946            // call to HttpFormEntry.showUI will return false and break out of
2947            // the while loop without talking to the HTTP server.
2948            dialog = GuiUtils.createDialog(LogUtil.getCurrentWindow(),
2949                                           "Support Request Form", false);
2950//            alreadyHaveDialog = false;
2951        }
2952
2953        JLabel statusLabel = GuiUtils.cLabel(" ");
2954        JComponent bottom = LayoutUtil.vbox(LayoutUtil.leftVbox(checkboxes), statusLabel);
2955
2956        while (true) {
2957            //Show form. Check if user pressed cancel.
2958            statusLabel.setText(" ");
2959            if ( !HttpFormEntry.showUI(entries, LayoutUtil.inset(topLabel, 10),
2960                                       bottom, dialog, alreadyHaveDialog)) {
2961                break;
2962            }
2963            statusLabel.setText("Posting support request...");
2964
2965            //Save persistent state
2966            getStore().put(PROP_HELP_NAME, nameEntry.getValue());
2967            getStore().put(PROP_HELP_ORG, orgEntry.getValue());
2968            getStore().put(PROP_HELP_EMAIL, emailEntry.getValue());
2969            getStore().save();
2970
2971            List<HttpFormEntry> entriesToPost = 
2972                new ArrayList<>(entries);
2973
2974            if ((stackTrace != null) && (stackTrace.length() > 0)) {
2975                entriesToPost.remove(descriptionEntry);
2976                String newDescription =
2977                    descriptionEntry.getValue()
2978                    + "\n\n******************\nStack trace:\n" + stackTrace;
2979                entriesToPost.add(
2980                    new HttpFormEntry(
2981                        HttpFormEntry.TYPE_HIDDEN, "form_data[description]",
2982                        "Description:", newDescription, 5, 30, true));
2983            }
2984
2985            try {
2986                extra.append(idv.getPluginManager().getPluginHtml());
2987                extra.append(getResourceManager().getHtmlView());
2988
2989                entriesToPost.add(new HttpFormEntry("form_data[att_extra]",
2990                    "extra.html", extra.toString().getBytes()));
2991
2992                if (includeBundleCbx.isSelected()) {
2993                    entriesToPost.add(
2994                        new HttpFormEntry(
2995                            "form_data[att_state]", "bundle" + Constants.SUFFIX_MCV,
2996                            idv.getPersistenceManager().getBundleXml(
2997                                true).getBytes()));
2998                }
2999                entriesToPost.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 
3000                    "form_data[cc_user]", "", 
3001                    Boolean.toString(getStore().get("mcv.supportreq.cc", true))));
3002
3003                String[] results = 
3004                    HttpFormEntry.doPost(entriesToPost, SUPPORT_REQ_URL);
3005
3006                if (results[0] != null) {
3007                    GuiUtils.showHtmlDialog(
3008                        results[0], "Support Request Response - Error",
3009                        "Support Request Response - Error", null, true);
3010                    continue;
3011                }
3012                String html = results[1];
3013                if (html.toLowerCase().indexOf("your email has been sent")
3014                        >= 0) {
3015                    LogUtil.userMessage("Your support request has been sent");
3016                    break;
3017                } else if (html.toLowerCase().indexOf("required fields")
3018                           >= 0) {
3019                    LogUtil.userErrorMessage(
3020                        "<html>There was a problem submitting your request. <br>Is your email correct?</html>");
3021                } else {
3022                    GuiUtils.showHtmlDialog(
3023                        html, "Unknown Support Request Response",
3024                        "Unknown Support Request Response", null, true);
3025                    System.err.println(html.toLowerCase());
3026                }
3027            } catch (Exception exc) {
3028                LogUtil.logException("Doing support request form", exc);
3029            }
3030        }
3031        dialog.dispose();
3032    }
3033
3034    @Override protected IdvXmlUi doMakeIdvXmlUi(IdvWindow window, 
3035        List viewManagers, Element skinRoot) 
3036    {
3037        return new McIDASVXmlUi(window, viewManagers, idv, skinRoot);
3038    }
3039
3040    /**
3041     * DeInitialize the given menu before it is shown
3042     * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu)
3043     */
3044    @Override
3045    protected void handleMenuDeSelected(final String id, final JMenu menu, final IdvWindow idvWindow) {
3046        super.handleMenuDeSelected(id, menu, idvWindow);
3047    }
3048
3049    /**
3050     * Initialize the given menu before it is shown
3051     * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu)
3052     */
3053    @Override
3054    protected void handleMenuSelected(final String id, final JMenu menu, final IdvWindow idvWindow) {
3055        if (id.equals(MENU_NEWVIEWS)) {
3056            ViewManager last = getVMManager().getLastActiveViewManager();
3057            menu.removeAll();
3058            makeViewStateMenu(menu, last);
3059        } else if (id.equals("bundles")) {
3060            menu.removeAll();
3061            makeBundleMenu(menu);
3062        } else if (id.equals(MENU_NEWDISPLAY_TAB)) {
3063            menu.removeAll();
3064            doMakeNewDisplayMenu(menu, false);
3065        } else if (id.equals(MENU_NEWDISPLAY)) {
3066            menu.removeAll();
3067            doMakeNewDisplayMenu(menu, true);
3068        } else if (id.equals("menu.tools.projections.deletesaved")) {
3069            menu.removeAll();
3070            makeDeleteViewsMenu(menu);
3071        } else if (id.equals("file.default.layout")) {
3072            makeDefaultLayoutMenu(menu);
3073        } else if (id.equals("tools.formulas")) {
3074            menu.removeAll();
3075            makeFormulasMenu(menu);
3076        } else {
3077            super.handleMenuSelected(id, menu, idvWindow);
3078        }
3079    }
3080
3081    /** A cache of the operand name to value for the user choices */
3082    private Map operandCache;
3083
3084    @Override public List selectUserChoices(String msg, List userOperands) {
3085        if (operandCache == null) {
3086            operandCache =
3087                (Hashtable) getStore().getEncodedFile("operandcache.xml");
3088            if (operandCache == null) {
3089                operandCache = new Hashtable();
3090            }
3091        }
3092        List fields         = new ArrayList();
3093        List components     = new ArrayList();
3094        List persistentCbxs = new ArrayList();
3095        components.add(new JLabel("Property"));
3096        components.add(new JLabel("Value"));
3097        components.add(new JLabel("Save in Bundle"));
3098        for (int i = 0; i < userOperands.size(); i++) {
3099            DataOperand operand   = (DataOperand)userOperands.get(i);
3100            String      fieldType = operand.getProperty("type");
3101            if (fieldType == null) {
3102                fieldType = FIELDTYPE_TEXT;
3103            }
3104            DerivedDataChoice formula = operand.getDataChoice();
3105            String description = operand.getDescription();
3106            if (formula != null) {
3107                description = formula.toString();
3108            }
3109
3110            String label = operand.getLabel();
3111            Object dflt = operand.getUserDefault();
3112            Object cacheKeyNewStyle = Misc.newList(description, label, fieldType);
3113            Object cacheKey = Misc.newList(label, fieldType);
3114
3115            Object cachedOperand = null;
3116            boolean oldStyle = operandCache.containsKey(cacheKey);
3117            boolean newStyle = operandCache.containsKey(cacheKeyNewStyle);
3118
3119            // if new style, always use that and ignore old style
3120            // if no new style, proceed as before.
3121            if (newStyle) {
3122                cachedOperand = operandCache.get(cacheKeyNewStyle);
3123            } else if (oldStyle) {
3124                cachedOperand = operandCache.get(cacheKey);
3125            }
3126
3127            if (cachedOperand != null) {
3128                dflt = cachedOperand;
3129            }
3130
3131            JCheckBox cbx = new JCheckBox("", operand.isPersistent());
3132            persistentCbxs.add(cbx);
3133            JComponent field     = null;
3134            JComponent fieldComp = null;
3135            if (fieldType.equals(FIELDTYPE_TEXT)) {
3136                String rowString = operand.getProperty("rows");
3137                if (rowString == null) {
3138                    rowString = "1";
3139                }
3140                int rows = Integer.parseInt(rowString);
3141                if (rows == 1) {
3142                    field = new JTextField((dflt != null)
3143                        ? dflt.toString()
3144                        : "", 15);
3145                } else {
3146                    field     = new JTextArea((dflt != null)
3147                        ? dflt.toString()
3148                        : "", rows, 15);
3149                    fieldComp = GuiUtils.makeScrollPane(field, 200, 100);
3150                }
3151            } else if (fieldType.equals(FIELDTYPE_BOOLEAN)) {
3152                field = new JCheckBox("", ((dflt != null)
3153                    ? new Boolean(
3154                    dflt.toString()).booleanValue()
3155                    : true));
3156            } else if (fieldType.equals(FIELDTYPE_CHOICE)) {
3157                String choices = operand.getProperty("choices");
3158                if (choices == null) {
3159                    throw new IllegalArgumentException(
3160                        "No 'choices' attribute defined for operand: "
3161                            + operand);
3162                }
3163                List l = StringUtil.split(choices, ";", true, true);
3164                field = new JComboBox(new Vector(l));
3165                if ((dflt != null) && l.contains(dflt)) {
3166                    ((JComboBox) field).setSelectedItem(dflt);
3167                }
3168            } else if (fieldType.equals(FIELDTYPE_FILE)) {
3169                JTextField fileFld = new JTextField(((dflt != null)
3170                    ? dflt.toString()
3171                    : ""), 30);
3172                field = fileFld;
3173                String patterns = operand.getProperty("filepattern");
3174                List   filters  = null;
3175                if (patterns != null) {
3176                    filters = new ArrayList();
3177                    List toks = StringUtil.split(patterns, ";", true, true);
3178                    for (int tokIdx = 0; tokIdx < toks.size(); tokIdx++) {
3179                        String tok   = (String) toks.get(tokIdx);
3180                        List subToks = StringUtil.split(tok, ":", true, true);
3181                        if (subToks.size() == 2) {
3182                            filters.add(
3183                                new PatternFileFilter(
3184                                    (String)subToks.get(0),
3185                                    (String)subToks.get(1)));
3186                        } else {
3187                            filters.add(new PatternFileFilter(tok, tok));
3188                        }
3189                    }
3190                }
3191                fieldComp = GuiUtils.centerRight(GuiUtils.hfill(fileFld),
3192                    GuiUtils.makeFileBrowseButton(fileFld, filters));
3193            } else if (fieldType.equals(FIELDTYPE_LOCATION)) {
3194                List l = ((dflt != null)
3195                    ? StringUtil.split(dflt.toString(), ";", true, true)
3196                    : (List) new ArrayList());
3197                final LatLonWidget llw = new LatLonWidget();
3198                field = llw;
3199                if (l.size() == 2) {
3200                    llw.setLat(Misc.decodeLatLon(l.get(0).toString()));
3201                    llw.setLon(Misc.decodeLatLon(l.get(1).toString()));
3202                }
3203                final JButton centerPopupBtn =
3204                    GuiUtils.getImageButton("/auxdata/ui/icons/Map16.gif",
3205                        getClass());
3206                centerPopupBtn.setToolTipText("Center on current displays");
3207                centerPopupBtn.addActionListener(ae -> popupCenterMenu(centerPopupBtn, llw));
3208                JComponent centerPopup = GuiUtils.inset(centerPopupBtn,
3209                    new Insets(0, 0, 0, 4));
3210                fieldComp = GuiUtils.hbox(llw, centerPopup);
3211            } else if (fieldType.equals(FIELDTYPE_AREA)) {
3212                //TODO:
3213            } else {
3214                throw new IllegalArgumentException("Unknown type: "
3215                    + fieldType + " for operand: " + operand);
3216            }
3217
3218            fields.add(field);
3219            label = StringUtil.replace(label, "_", " ");
3220            components.add(GuiUtils.rLabel(label));
3221            components.add((fieldComp != null)
3222                ? fieldComp
3223                : field);
3224            components.add(cbx);
3225        }
3226        //        GuiUtils.tmpColFills = new int[] { GridBagConstraints.HORIZONTAL,
3227        //                                           GridBagConstraints.NONE,
3228        //                                           GridBagConstraints.NONE };
3229        GuiUtils.tmpInsets = GuiUtils.INSETS_5;
3230        Component contents = GuiUtils.topCenter(new JLabel(msg),
3231            GuiUtils.doLayout(components, 3,
3232                GuiUtils.WT_NYN, GuiUtils.WT_N));
3233        if ( !GuiUtils.showOkCancelDialog(null, "Select input", contents,
3234            null, fields)) {
3235            return null;
3236        }
3237        List values = new ArrayList();
3238        for (int i = 0; i < userOperands.size(); i++) {
3239            DataOperand operand = (DataOperand) userOperands.get(i);
3240            String description = operand.getDescription();
3241            DerivedDataChoice formula = operand.getDataChoice();
3242            String label = operand.getLabel();
3243            Object field = fields.get(i);
3244            Object value = null;
3245            Object cacheValue = null;
3246
3247            if (formula != null) {
3248                description = formula.toString();
3249            }
3250
3251            if (field instanceof JTextComponent) {
3252                value = ((JTextComponent) field).getText().trim();
3253            } else if (field instanceof JCheckBox) {
3254                value = Boolean.valueOf(((JCheckBox)field).isSelected());
3255            } else if (field instanceof JComboBox) {
3256                value = ((JComboBox) field).getSelectedItem();
3257            } else if (field instanceof LatLonWidget) {
3258                LatLonWidget llw = (LatLonWidget) field;
3259                value      = new LatLonPointImpl(llw.getLat(), llw.getLon());
3260                cacheValue = llw.getLat() + ";" + llw.getLon();
3261            } else {
3262                throw new IllegalArgumentException("Unknown field type:"
3263                    + field.getClass().getName());
3264            }
3265            if (cacheValue == null) {
3266                cacheValue = value;
3267            }
3268            JCheckBox cbx       = (JCheckBox)persistentCbxs.get(i);
3269            String    fieldType = operand.getProperty("type");
3270            if (fieldType == null) {
3271                fieldType = "text";
3272            }
3273
3274            Object cacheKey = Misc.newList(description, label, fieldType);
3275            operandCache.put(cacheKey, cacheValue);
3276            values.add(new UserOperandValue(value, cbx.isSelected()));
3277        }
3278        getStore().putEncodedFile("operandcache.xml", operandCache);
3279        return values;
3280    }
3281
3282    private boolean didTabs = false;
3283    private boolean didNewWindow = false;
3284
3285    public void makeDefaultLayoutMenu(final JMenu menu) {
3286        if (menu == null)
3287            throw new NullPointerException("Must provide a non-null default layout menu");
3288
3289        menu.removeAll();
3290        JMenuItem saveLayout = new JMenuItem("Save");
3291        McVGuiUtils.setMenuImage(saveLayout, Constants.ICON_DEFAULTLAYOUTADD_SMALL);
3292        saveLayout.setToolTipText("Save as default layout");
3293        saveLayout.addActionListener(e -> ((McIDASV)idv).doSaveAsDefaultLayout());
3294
3295        JMenuItem removeLayout = new JMenuItem("Remove");
3296        McVGuiUtils.setMenuImage(removeLayout, Constants.ICON_DEFAULTLAYOUTDELETE_SMALL);
3297        removeLayout.setToolTipText("Remove saved default layout");
3298        removeLayout.addActionListener(e -> idv.doClearDefaults());
3299
3300        removeLayout.setEnabled(((McIDASV)idv).hasDefaultLayout());
3301
3302        menu.add(saveLayout);
3303        menu.add(removeLayout);
3304    }
3305
3306    /**
3307     * Bundles any compatible {@link ViewManager} states into
3308     * {@link JMenuItem JMenuItem} and adds said menu items to {@code menu}.
3309     * Incompatible states are ignored.
3310     * 
3311     * <p>Each {@code JMenuItem} (except those under the {@literal "Delete"}
3312     * menu--apologies) associates a {@literal "view state"} and an
3313     * {@link ObjectListener}. The {@code ObjectListener} uses this associated
3314     * view state to attempt reinitialization of {@code vm}.
3315     * 
3316     * <p>Override reasoning:
3317     * <ul>
3318     *   <li>
3319     *     terminology ({@literal "views"} rather than {@literal "viewpoints"}).
3320     *   </li>
3321     *   <li>
3322     *     use of {@link #filterVMMStatesWithVM(ViewManager, Collection)} to
3323     *     properly detect the {@literal "no saved views"} case.
3324     *   </li>
3325     * </ul>
3326     * 
3327     * @param menu Menu to populate. Should not be {@code null}.
3328     * @param vm {@code ViewManager} that might get reinitialized.
3329     * Should not be {@code null}.
3330     * 
3331     * @see ViewManager#initWith(ViewManager, boolean)
3332     * @see ViewManager#initWith(ViewState)
3333     */
3334    @Override public void makeViewStateMenu(final JMenu menu, final ViewManager vm) {
3335        List<TwoFacedObject> vmStates =
3336            filterVMMStatesWithVM(vm, getVMManager().getVMState());
3337        if (vmStates.isEmpty()) {
3338            JMenuItem item = new JMenuItem(Msg.msg("No Saved Views"));
3339            item.setEnabled(false);
3340            menu.add(item);
3341        } else {
3342            JMenu deleteMenu = new JMenu("Delete");
3343            makeDeleteViewsMenu(deleteMenu);
3344            menu.add(deleteMenu);
3345        }
3346
3347        // McIDAS Inquiry #2803-3141 
3348        ArrayList<TwoFacedObject> notInDirs = new ArrayList<>();
3349        HashMap<String, JMenu> dirs = new HashMap<>();
3350
3351        for (int i = 0; i < vmStates.size(); i++) {
3352            TwoFacedObject tfo = vmStates.get(i);
3353
3354            if (tfo.getLabel().toString().contains(">")) {
3355                String dirStr = tfo.getLabel().toString();
3356                String[] dirAsArray = dirStr.split(">");
3357
3358                if (dirAsArray.length > 2) {
3359                    String tgt = "";
3360                    for (int split = 1; split < dirAsArray.length; split++) {
3361                        tgt += dirAsArray[split] + ">";
3362                    }
3363                    dirAsArray[1] = tgt;
3364                }
3365
3366                JMenu tmp;
3367                if (dirs.containsKey(dirAsArray[0])) {
3368                    tmp = dirs.get(dirAsArray[0]);
3369                } else {
3370                    tmp = new JMenu(dirAsArray[0]);
3371                }
3372                JMenuItem mi = new JMenuItem(dirAsArray[1]);
3373                tmp.add(mi);
3374                dirs.put(dirAsArray[0], tmp);
3375
3376                mi.addActionListener(new ObjectListener(tfo.getId()) {
3377                    public void actionPerformed(final ActionEvent e) {
3378                        if (vm == null) {
3379                            return;
3380                        }
3381                        if (theObject instanceof ViewManager) {
3382                            vm.initWith((ViewManager)theObject, true);
3383                        } else if (theObject instanceof ViewState) {
3384                            try {
3385                                vm.initWith((ViewState)theObject);
3386                            } catch (Throwable ex) {
3387                                logException("Initializing view with ViewState", ex);
3388                            }
3389                        } else {
3390                            LogUtil.consoleMessage("UIManager.makeViewStateMenu: Object of unknown type: "+theObject.getClass().getName());
3391                        }
3392                    }
3393                });
3394
3395            } else {
3396                notInDirs.add(tfo);
3397            }
3398        }
3399
3400        ArrayList<JMenu> folders = new ArrayList<>();
3401
3402        for (Map.Entry<String, JMenu> entry : dirs.entrySet()) {
3403            JMenu jmu = entry.getValue();
3404            ArrayList<JMenuItem> itms = new ArrayList<>();
3405
3406            for (int i = 0; i < jmu.getItemCount(); i++) {
3407                itms.add(jmu.getItem(i));
3408            }
3409
3410            Collections.sort(itms, new Comparator<JMenuItem>() {
3411                @Override
3412                public int compare(JMenuItem o1, JMenuItem o2) {
3413                    return (o1.getText().toLowerCase().compareTo(o2.getText().toLowerCase()));
3414                }
3415            });
3416
3417            JMenu out = new JMenu(jmu.getText());
3418
3419            for (int i = 0; i < itms.size(); i++) {
3420                out.add(itms.get(i));
3421            }
3422
3423            folders.add(out);
3424        }
3425
3426        Collections.sort(folders, new Comparator<JMenu>() {
3427            @Override
3428            public int compare(JMenu o1, JMenu o2) {
3429                return (o1.getText().toLowerCase().compareTo(o2.getText().toLowerCase()));
3430            }
3431        });
3432
3433        for (int i = 0; i < folders.size(); i++) {
3434            menu.add(folders.get(i));
3435        }
3436
3437        if (folders.size() > 0) {
3438            menu.addSeparator();
3439        }
3440
3441        Collections.sort(notInDirs, new  Comparator<TwoFacedObject>() {
3442            @Override
3443            public int compare(TwoFacedObject a, TwoFacedObject b) {
3444                return (a.getLabel().toString().toLowerCase().compareTo(b.getLabel().toString().toLowerCase()));
3445            }
3446        });
3447
3448        for (TwoFacedObject tfo : notInDirs) {
3449            JMenuItem mi = new JMenuItem(tfo.getLabel().toString());
3450            menu.add(mi);
3451            mi.addActionListener(new ObjectListener(tfo.getId()) {
3452                public void actionPerformed(final ActionEvent e) {
3453                    if (vm == null) {
3454                        return;
3455                    }
3456
3457                    if (theObject instanceof ViewManager) {
3458                        vm.initWith((ViewManager)theObject, true);
3459                    } else if (theObject instanceof ViewState) {
3460                        try {
3461                            vm.initWith((ViewState)theObject);
3462                        } catch (Throwable ex) {
3463                            logException("Initializing view with ViewState", ex);
3464                        }
3465                    } else {
3466                        LogUtil.consoleMessage("UIManager.makeViewStateMenu: Object of unknown type: "+theObject.getClass().getName());
3467                    }
3468                }
3469            });
3470        }
3471
3472        // the "3" ensures that the "save viewpoint" menu item, the separator,
3473        // and the "delete" menu item are fixed at the top.
3474        new MenuScroller(menu, menu, 125, 3);
3475    }
3476
3477    /**
3478     * Overridden by McIDAS-V to add menu scrolling functionality to the
3479     * {@literal "delete"} submenu.
3480     *
3481     * @param menu {@literal "Delete"} submenu.
3482     */
3483    @Override public void makeDeleteViewsMenu(JMenu menu) {
3484        super.makeDeleteViewsMenu(menu);
3485        new MenuScroller(menu, menu, 125);
3486    }
3487
3488    /**
3489     * Returns a list of {@link TwoFacedObject}s that are known to be 
3490     * compatible with {@code vm}.
3491     * 
3492     * <p>This method is currently capable of dealing with
3493     * {@link TwoFacedObject TwoFacedObjects} and
3494     * {@link ViewState ViewStates} within {@code states}. Any other types are
3495     * ignored.
3496     * 
3497     * @param vm {@link ViewManager} to use for compatibility tests.
3498     * {@code null} is allowed.
3499     * @param states Collection of objects to test against {@code vm}.
3500     * {@code null} is allowed.
3501     * 
3502     * @return Either a {@link List} of compatible {@literal "view states"}
3503     * or an empty {@code List}.
3504     * 
3505     * @see ViewManager#isCompatibleWith(ViewManager)
3506     * @see ViewManager#isCompatibleWith(ViewState)
3507     * @see #makeViewStateMenu(JMenu, ViewManager)
3508     */
3509    public static List<TwoFacedObject> filterVMMStatesWithVM(final ViewManager vm, final Collection<?> states) {
3510        if ((vm == null) || (states == null) || states.isEmpty()) {
3511            return Collections.emptyList();
3512        }
3513
3514        List<TwoFacedObject> validStates = new ArrayList<>(states.size());
3515        for (Object obj : states) {
3516            TwoFacedObject tfo = null;
3517            if (obj instanceof TwoFacedObject) {
3518                tfo = (TwoFacedObject)obj;
3519                if (vm.isCompatibleWith((ViewManager)tfo.getId())) {
3520                    continue;
3521                }
3522            } else if (obj instanceof ViewState) {
3523                if (!vm.isCompatibleWith((ViewState)obj)) {
3524                    continue;
3525                }
3526                tfo = new TwoFacedObject(((ViewState)obj).getName(), obj);
3527            } else {
3528                LogUtil.consoleMessage("UIManager.filterVMMStatesWithVM: Object of unknown type: "+obj.getClass().getName());
3529                continue;
3530            }
3531            validStates.add(tfo);
3532        }
3533        return validStates;
3534    }
3535
3536    /**
3537     * Overridden to build a custom Display menu.
3538     * @see ucar.unidata.idv.ui.IdvUIManager#initializeDisplayMenu(JMenu)
3539     */
3540    @Override protected void initializeDisplayMenu(JMenu displayMenu) {
3541        JMenu m;
3542        JMenuItem mi;
3543
3544        // Get the list of possible standalone control descriptors
3545        Hashtable controlsHash = new Hashtable();
3546        List controlDescriptors = getStandAloneControlDescriptors();
3547        for (int i = 0; i < controlDescriptors.size(); i++) {
3548            ControlDescriptor cd = (ControlDescriptor)controlDescriptors.get(i);
3549            String cdLabel = cd.getLabel();
3550            if (cdLabel.equals("Range Rings")) {
3551                controlsHash.put(cdLabel, cd);
3552            } else if (cdLabel.equals("Range and Bearing")) {
3553                controlsHash.put(cdLabel, cd);
3554            } else if (cdLabel.equals("Location Indicator")) {
3555                controlsHash.put(cdLabel, cd);
3556            } else if (cdLabel.equals("Drawing Control")) {
3557                controlsHash.put(cdLabel, cd);
3558            } else if (cdLabel.equals("Transect Drawing Control")) {
3559                controlsHash.put(cdLabel, cd);
3560            }
3561        }
3562
3563        // Build the menu
3564        ControlDescriptor cd;
3565
3566        mi = new JMenuItem("Create Layer from Data Source...");
3567        mi.addActionListener(ae -> showDashboard("Data Sources"));
3568        displayMenu.add(mi);
3569
3570        mi = new JMenuItem("Layer Controls...");
3571        mi.addActionListener(ae -> showDashboard("Layer Controls"));
3572        displayMenu.add(mi);
3573
3574        displayMenu.addSeparator();
3575
3576        cd = (ControlDescriptor)controlsHash.get("Range Rings");
3577        mi = makeControlDescriptorItem(cd);
3578        mi.setText("Add Range Rings");
3579        displayMenu.add(mi);
3580
3581        cd = (ControlDescriptor)controlsHash.get("Range and Bearing");
3582        mi = makeControlDescriptorItem(cd);
3583        McVGuiUtils.setMenuImage(mi, Constants.ICON_RANGEANDBEARING_SMALL);
3584        mi.setText("Add Range and Bearing");
3585        displayMenu.add(mi);
3586
3587        displayMenu.addSeparator();
3588
3589        cd = (ControlDescriptor)controlsHash.get("Transect Drawing Control");
3590        mi = makeControlDescriptorItem(cd);
3591        mi.setText("Draw Transect...");
3592        displayMenu.add(mi);
3593
3594        cd = (ControlDescriptor)controlsHash.get("Drawing Control");
3595        mi = makeControlDescriptorItem(cd);
3596        mi.setText("Draw Freely...");
3597        displayMenu.add(mi);
3598
3599        displayMenu.addSeparator();
3600
3601        cd = (ControlDescriptor)controlsHash.get("Location Indicator");
3602        mi = makeControlDescriptorItem(cd);
3603        McVGuiUtils.setMenuImage(mi, Constants.ICON_LOCATION_SMALL);
3604        mi.setText("Add Location Indicator");
3605        displayMenu.add(mi);
3606
3607        ControlDescriptor locationDescriptor = idv.getControlDescriptor("locationcontrol");
3608        if (locationDescriptor != null) {
3609            List stations = idv.getLocationList();
3610            ObjectListener listener = new ObjectListener(locationDescriptor) {
3611                public void actionPerformed(ActionEvent ae, Object obj) {
3612                    addStationDisplay((NamedStationTable) obj, (ControlDescriptor) theObject);
3613                }
3614            };
3615            List menuItems = NamedStationTable.makeMenuItems(stations, listener);
3616            displayMenu.add(MenuUtil.makeMenu("Plot Location Labels", menuItems));
3617        }
3618
3619        displayMenu.addSeparator();
3620
3621        mi = new JMenuItem("Add Background Image");
3622        McVGuiUtils.setMenuImage(mi, Constants.ICON_BACKGROUND_SMALL);
3623        mi.addActionListener(ae -> getIdv().doMakeBackgroundImage());
3624        displayMenu.add(mi);
3625
3626        mi = new JMenuItem("Reset Map Layer to Defaults");
3627        mi.addActionListener(ae -> {
3628            // TODO: Call IdvUIManager.addDefaultMap()... should be made private
3629//                addDefaultMap();
3630            ControlDescriptor mapDescriptor = idv.getControlDescriptor("mapdisplay");
3631            if (mapDescriptor == null) {
3632                return;
3633            }
3634            String attrs = "initializeAsDefault=true;displayName=Default Background Maps;";
3635            idv.doMakeControl(new ArrayList(), mapDescriptor, attrs, null);
3636        });
3637        displayMenu.add(mi);
3638        Msg.translateTree(displayMenu);
3639    }
3640
3641    /**
3642     * Get the window title from the skin
3643     *
3644     * @param index  the skin index
3645     *
3646     * @return  the title
3647     */
3648    private String getWindowTitleFromSkin(final int index) {
3649        if (!skinToTitle.containsKey(index)) {
3650            IdvResourceManager mngr = getResourceManager();
3651            XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN);
3652            List<String> names = StringUtil.split(skins.getShortName(index), ">", true, true);
3653            String title = getStateManager().getTitle();
3654            if (!names.isEmpty()) {
3655                title = title + " - " + StringUtil.join(" - ", names);
3656            }
3657            skinToTitle.put(index, title);
3658        }
3659        return skinToTitle.get(index);
3660    }
3661
3662    @SuppressWarnings("unchecked")
3663    @Override public Hashtable getMenuIds() {
3664        return menuIds;
3665    }
3666
3667    @SuppressWarnings("unchecked")
3668    @Override public JMenuBar doMakeMenuBar(final IdvWindow idvWindow) {
3669        Hashtable<String, JMenuItem> menuMap = new Hashtable<>();
3670        JMenuBar menuBar = new JMenuBar();
3671        final IdvResourceManager mngr = getResourceManager();
3672        XmlResourceCollection xrc = mngr.getXmlResources(mngr.RSC_MENUBAR);
3673        Hashtable<String, ImageIcon> actionIcons = new Hashtable<>();
3674
3675        for (int i = 0; i < xrc.size(); i++) {
3676            GuiUtils.processXmlMenuBar(xrc.getRoot(i), menuBar, getIdv(), menuMap, actionIcons);
3677        }
3678
3679        menuIds = new Hashtable<>(menuMap);
3680
3681        // Ensure that the "help" menu is the last menu.
3682        JMenuItem helpMenu = menuMap.get(MENU_HELP);
3683        if (helpMenu != null) {
3684            menuBar.remove(helpMenu);
3685            menuBar.add(helpMenu);
3686        }
3687
3688        //TODO: Perhaps we will put the different skins in the menu?
3689        JMenu newDisplayMenu = (JMenu)menuMap.get(MENU_NEWDISPLAY);
3690        if (newDisplayMenu != null) {
3691            MenuUtil.makeMenu(newDisplayMenu, makeSkinMenuItems(makeMenuBarActionListener(), true, false));
3692        }
3693
3694//        final JMenu publishMenu = menuMap.get(MENU_PUBLISH);
3695//        if (publishMenu != null) {
3696//            if (!getPublishManager().isPublishingEnabled())
3697//                publishMenu.getParent().remove(publishMenu);
3698//            else
3699//                getPublishManager().initMenu(publishMenu);
3700//        }
3701
3702        for (Entry<String, JMenuItem> e : menuMap.entrySet()) {
3703            if (!(e.getValue() instanceof JMenu)) {
3704                continue;
3705            }
3706            String menuId = e.getKey();
3707            JMenu menu = (JMenu)e.getValue();
3708            menu.addMenuListener(makeMenuBarListener(menuId, menu, idvWindow));
3709        }
3710        return menuBar;
3711    }
3712
3713    private final ActionListener makeMenuBarActionListener() {
3714        final IdvResourceManager mngr = getResourceManager();
3715        return ae -> {
3716            XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN);
3717            int skinIndex = ((Integer)ae.getSource()).intValue();
3718            createNewWindow(null, true, getWindowTitleFromSkin(skinIndex),
3719                skins.get(skinIndex).toString(),
3720                skins.getRoot(skinIndex, false), true, null);
3721        };
3722    }
3723
3724    private final MenuListener makeMenuBarListener(final String id, final JMenu menu, final IdvWindow idvWindow) {
3725        return new MenuListener() {
3726            public void menuCanceled(final MenuEvent e) { }
3727            public void menuDeselected(final MenuEvent e) { handleMenuDeSelected(id, menu, idvWindow); }
3728            public void menuSelected(final MenuEvent e) { handleMenuSelected(id, menu, idvWindow); }
3729        };
3730    }
3731
3732    /**
3733     * Handle mouse clicks that occur within the toolbar.
3734     */
3735    private class PopupListener extends MouseAdapter {
3736
3737        private JPopupMenu popup;
3738
3739        public PopupListener(JPopupMenu p) {
3740            popup = p;
3741        }
3742
3743        // handle right clicks on os x and linux
3744        public void mousePressed(MouseEvent e) {
3745            if (e.isPopupTrigger()) {
3746                popup.show(e.getComponent(), e.getX(), e.getY());
3747            }
3748        }
3749
3750        // Windows doesn't seem to trigger mousePressed() for right clicks, but
3751        // never fear; mouseReleased() does the job.
3752        public void mouseReleased(MouseEvent e) {
3753            if (e.isPopupTrigger()) {
3754                popup.show(e.getComponent(), e.getX(), e.getY());
3755            }
3756        }
3757    }
3758
3759    /**
3760     * Handle (polymorphically) the {@link DataControlDialog}.
3761     *
3762     * This dialog is used to either select a display control to create
3763     * or is used to set the timers used for a {@link ucar.unidata.data.DataSource}.
3764     *
3765     * @param dcd The dialog
3766     */
3767    public void processDialog(DataControlDialog dcd) {
3768        int estimatedMB = getEstimatedMegabytes(dcd);
3769        if (estimatedMB > 0) {
3770            double totalMem = Runtime.getRuntime().maxMemory();
3771            double highMem = Runtime.getRuntime().totalMemory();
3772            double freeMem = Runtime.getRuntime().freeMemory();
3773            double usedMem = (highMem - freeMem);
3774            int availableMB = Math.round( ((float)totalMem - (float)usedMem) / 1024f / 1024f);
3775            int percentOfAvailable = Math.round((float)estimatedMB / (float)availableMB * 100f);
3776            if (percentOfAvailable > 95) {
3777                String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>";
3778                message += "which exceeds 95% of total amount available (" + availableMB +"MB).<br>";
3779                message += "Data load cancelled.</html>";
3780                JComponent msgLabel = new JLabel(message);
3781                GuiUtils.showDialog("Data Size", msgLabel);
3782                return;
3783            } else if (percentOfAvailable >= 75) {
3784                String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>";
3785                message += percentOfAvailable + "% of the total amount available (" + availableMB + "MB).<br>";
3786                message += "Continue loading data?</html>";
3787                JComponent msgLabel = new JLabel(message);
3788                if (!GuiUtils.askOkCancel("Data Size", msgLabel)) {
3789                    return;
3790                }
3791            }
3792        }
3793        super.processDialog(dcd);
3794    }
3795
3796    /**
3797     * Estimate the number of megabytes that will be used by this data selection
3798     * 
3799     * @param dcd Data control dialog containing the data selection whose size
3800     *            we'd like to estimate. Cannot be {@code null}.
3801     *            
3802     * @return Rough estimate of the size (in megabytes) of the 
3803     * {@code DataSelection} within {@code dcd}.
3804     */
3805    protected int getEstimatedMegabytes(DataControlDialog dcd) {
3806        int estimatedMB = 0;
3807        DataChoice dataChoice = dcd.getDataChoice();
3808        if (dataChoice != null) {
3809            Object[] selectedControls = dcd.getSelectedControls();
3810            for (int i = 0; i < selectedControls.length; i++) {
3811                ControlDescriptor cd = (ControlDescriptor) selectedControls[i];
3812
3813                //Check if the data selection is ok
3814                if(!dcd.getDataSelectionWidget().okToCreateTheDisplay(cd.doesLevels())) {
3815                    continue;
3816                }
3817
3818                DataSelection dataSelection = dcd.getDataSelectionWidget().createDataSelection(cd.doesLevels());
3819                                
3820                // Get the size in pixels of the requested image
3821                Object gotSize = dataSelection.getProperty("SIZE");
3822                if (gotSize == null) {
3823                        continue;
3824                }
3825                List<String> dims = StringUtil.split(gotSize, " ", false, false);
3826                int myLines = -1;
3827                int myElements = -1;
3828                if (dims.size() == 2) {
3829                    try {
3830                        myLines = Integer.parseInt(dims.get(0));
3831                        myElements = Integer.parseInt(dims.get(1));
3832                    }
3833                    catch (Exception e) { }
3834                }
3835
3836                // Get the count of times requested
3837                int timeCount = 1;
3838                DataSelectionWidget dsw = dcd.getDataSelectionWidget();
3839                List times = dsw.getSelectedDateTimes();
3840                List timesAll = dsw.getAllDateTimes();
3841                if ((times != null) && !times.isEmpty()) {
3842                    timeCount = times.size();
3843                } else if ((timesAll != null) && !timesAll.isEmpty()) {
3844                    timeCount = timesAll.size();
3845                }
3846                
3847                // Total number of pixels
3848                // Assumed lines x elements x times x 4bytes
3849                // Empirically seems to be taking *twice* that (64bit fields??)
3850                float totalPixels = (float)myLines * (float)myElements * (float)timeCount;
3851                float totalBytes = totalPixels * 4 * 2;
3852                estimatedMB += Math.round(totalBytes / 1024f / 1024f);
3853
3854                int additionalMB = 0;
3855                // Empirical tests show that textures are not affecting
3856                // required memory... comment out for now
3857                /*
3858                int textureDimensions = 2048;
3859                int mbPerTexture = Math.round((float)textureDimensions * (float)textureDimensions * 4 / 1024f / 1024f);
3860                int textureCount = (int)Math.ceil((float)myLines / 2048f) * (int)Math.ceil((float)myElements / 2048f);
3861                int additionalMB = textureCount * mbPerTexture * timeCount;
3862                */
3863                estimatedMB += additionalMB;
3864            }
3865        }
3866        return estimatedMB;
3867    }
3868
3869    /**
3870     * Represents a SavedBundle as a tree.
3871     */
3872    private class BundleTreeNode {
3873
3874        private String name;
3875
3876        private SavedBundle bundle;
3877
3878        private List<BundleTreeNode> kids;
3879
3880        /**
3881         * This constructor is used to build a node that is considered a
3882         * {@literal "parent"}.
3883         *
3884         * These nodes only have child nodes, no {@code SavedBundles}. This
3885         * was done so that distinguishing between bundles and bundle
3886         * subcategories would be easy.
3887         * 
3888         * @param name The name of this node. For a parent node with
3889         * {@literal "Toolbar>cat"} as the path, the name parameter would
3890         * contain only {@literal "cat"}.
3891         */
3892        public BundleTreeNode(String name) {
3893            this(name, null);
3894        }
3895
3896        /**
3897         * Nodes constructed using this constructor can only ever be child
3898         * nodes.
3899         * 
3900         * @param name The name of the SavedBundle.
3901         * @param bundle A reference to the SavedBundle.
3902         */
3903        public BundleTreeNode(String name, SavedBundle bundle) {
3904            this.name = name;
3905            this.bundle = bundle;
3906            kids = new LinkedList<>();
3907        }
3908
3909        /**
3910         * @param child The node to be added to the current node.
3911         */
3912        public void addChild(BundleTreeNode child) {
3913            kids.add(child);
3914        }
3915
3916        /**
3917         * @return Returns all child nodes of this node.
3918         */
3919        public List<BundleTreeNode> getChildren() {
3920            return kids;
3921        }
3922
3923        /**
3924         * @return Return the SavedBundle associated with this node (if any).
3925         */
3926        public SavedBundle getBundle() {
3927            return bundle;
3928        }
3929
3930        /**
3931         * @return The name of this node.
3932         */
3933        public String getName() {
3934            return name;
3935        }
3936    }
3937
3938    /**
3939     * A type of {@code HttpFormEntry} that supports line wrapping for
3940     * text area entries.
3941     * 
3942     * @see HttpFormEntry
3943     */
3944    private static class FormEntry extends HttpFormEntry {
3945        /** Initial contents of this entry. */
3946        private String value = "";
3947
3948        /** Whether or not the JTextArea should wrap lines. */
3949        private boolean wrap = true;
3950
3951        /** Entry type. Used to remain compatible with the IDV. */
3952        private int type = HttpFormEntry.TYPE_AREA;
3953
3954        /** Number of rows in the JTextArea. */
3955        private int rows = 5;
3956
3957        /** Number of columns in the JTextArea. */
3958        private int cols = 30;
3959
3960        /** GUI representation of this entry. */
3961        private JTextArea component = new JTextArea(value, rows, cols);
3962
3963        /**
3964         * Required to keep Java happy.
3965         */
3966        public FormEntry() {
3967            super(HttpFormEntry.TYPE_AREA, "form_data[description]", 
3968                "Description:");
3969        }
3970
3971        /**
3972         * Using this constructor allows McIDAS-V to control whether or not a
3973         * HttpFormEntry performs line wrapping for JTextArea components.
3974         * 
3975         * @param wrap Whether or not line wrapping should be enabled.
3976         * @param type Type of this entry
3977         * @param name Name
3978         * @param label Label
3979         * @param value Initial value
3980         * @param rows Number of rows.
3981         * @param cols Number of columns.
3982         * @param required Whether or not the entry will be required.
3983         */
3984        public FormEntry(boolean wrap, int type, String name, String label, String value, int rows, int cols, boolean required) {
3985            super(type, name, label, value, rows, cols, required);
3986            this.type = type;
3987            this.rows = rows;
3988            this.cols = cols;
3989            this.wrap = wrap;
3990        }
3991
3992        /**
3993         * Overrides the IDV method so that the McIDAS-V support request form
3994         * will wrap lines in the "Description" field.
3995         * 
3996         * @param guiComps List to which this instance should be added.
3997         */
3998        @SuppressWarnings("unchecked")
3999        @Override public void addToGui(List guiComps) {
4000            if (type == HttpFormEntry.TYPE_AREA) {
4001                Dimension minSize = new Dimension(500, 200);
4002                guiComps.add(LayoutUtil.top(GuiUtils.rLabel(getLabel())));
4003                component.setLineWrap(wrap);
4004                component.setWrapStyleWord(wrap);
4005                JScrollPane sp = new JScrollPane(component);
4006                sp.setPreferredSize(minSize);
4007                sp.setMinimumSize(minSize);
4008                guiComps.add(sp);
4009            } else {
4010                super.addToGui(guiComps);
4011            }
4012        }
4013
4014        /**
4015         * Since the IDV doesn't provide a getComponent for 
4016         * {@code addToGui}, we must make our {@code component} field
4017         * local to this class. 
4018         * Hijacks any value requests so that the local {@code component}
4019         * field is queried, not the IDV's.
4020         * 
4021         * @return Contents of form.
4022         */
4023        @Override public String getValue() {
4024            if (type != HttpFormEntry.TYPE_AREA) {
4025                return super.getValue();
4026            }
4027            return component.getText();
4028        }
4029
4030        /**
4031         * Hijacks any requests to set the {@code component} field's text.
4032         */
4033        @Override public void setValue(final String newValue) {
4034            if (type == HttpFormEntry.TYPE_AREA) {
4035                component.setText(newValue);
4036            } else {
4037                super.setValue(newValue);
4038            }
4039        }
4040    }
4041
4042    /**
4043     * A {@code ToolbarStyle} is a representation of the way icons associated
4044     * with current toolbar actions should be displayed. This notion is so far
4045     * limited to the sizing of icons, but that may change.
4046     */
4047    public enum ToolbarStyle {
4048        /**
4049         * Represents the current toolbar actions as large icons. Currently,
4050         * {@literal "large"} is defined as {@code 32 x 32} pixels.
4051         */
4052        LARGE("Large Icons", "action.icons.large", 32),
4053
4054        /**
4055         * Represents the current toolbar actions as medium icons. Currently,
4056         * {@literal "medium"} is defined as {@code 22 x 22} pixels.
4057         */
4058        MEDIUM("Medium Icons", "action.icons.medium", 22),
4059
4060        /** 
4061         * Represents the current toolbar actions as small icons. Currently,
4062         * {@literal "small"} is defined as {@code 16 x 16} pixels. 
4063         */
4064        SMALL("Small Icons", "action.icons.small", 16);
4065
4066        /** Label to use in the toolbar customization popup menu. */
4067        private final String label;
4068
4069        /** Signals that the user selected a specific icon size. */
4070        private final String action;
4071
4072        /** Icon dimensions. Each icon should be {@code size * size}. */
4073        private final int size;
4074
4075        /**
4076         * {@link #size} in {@link String} form, merely for use with the IDV's
4077         * preference functionality.
4078         */
4079        private final String sizeAsString;
4080
4081        /**
4082         * Initializes a toolbar style.
4083         * 
4084         * @param label Label used in the toolbar popup menu.
4085         * @param action Command that signals the user selected this toolbar 
4086         * style.
4087         * @param size Dimensions of the icons.
4088         * 
4089         * @throws NullPointerException if {@code label} or {@code action} are
4090         * null.
4091         * 
4092         * @throws IllegalArgumentException if {@code size} is not positive.
4093         */
4094        ToolbarStyle(final String label, final String action, final int size) {
4095            requireNonNull(label, "Label cannot be null.");
4096            requireNonNull(action, "Action cannot be null.");
4097
4098            if (size <= 0) {
4099                throw new IllegalArgumentException("Size must be a positive integer");
4100            }
4101
4102            this.label = label;
4103            this.action = action;
4104            this.size = size;
4105            this.sizeAsString = Integer.toString(size);
4106        }
4107
4108        /**
4109         * Returns the label to use as a brief description of this style.
4110         * 
4111         * @return Description of style (suitable for a label).
4112         */
4113        public String getLabel() {
4114            return label;
4115        }
4116
4117        /**
4118         * Returns the action command associated with this style.
4119         * 
4120         * @return This style's {@literal "action command"}.
4121         */
4122        public String getAction() {
4123            return action;
4124        }
4125
4126        /**
4127         * Returns the dimensions of icons used in this style.
4128         * 
4129         * @return Dimensions of this style's icons.
4130         */
4131        public int getSize() {
4132            return size;
4133        }
4134
4135        /**
4136         * Returns {@link #size} as a {@link String} to make cooperating with
4137         * the IDV preferences code easier.
4138         * 
4139         * @return String representation of this style's icon dimensions.
4140         */
4141        public String getSizeAsString() {
4142            return sizeAsString;
4143        }
4144
4145        /**
4146         * Returns a brief description of this {@code ToolbarStyle}.
4147         *
4148         * <p>A typical example:
4149         * {@code [ToolbarStyle@1337: label="Large Icons", size=32]}
4150         * 
4151         * <p>Note that the format and details provided are subject to change.
4152         *
4153         * @return String representation of this {@code ToolbarStyle} instance.
4154         */
4155        public String toString() {
4156            return String.format("[ToolbarStyle@%x: label=%s, size=%d]", 
4157                hashCode(), label, size);
4158        }
4159
4160        /**
4161         * Convenience method for build the toolbar customization popup menu.
4162         * 
4163         * @param manager {@link UIManager} that will be listening for action
4164         * commands.
4165         * 
4166         * @return Menu item that has {@code manager} listening for 
4167         * {@link #action}.
4168         */
4169        protected JMenuItem buildMenuItem(final UIManager manager) {
4170            JMenuItem item = new JRadioButtonMenuItem(label);
4171            item.setActionCommand(action);
4172            item.addActionListener(manager);
4173            return item;
4174        }
4175    }
4176
4177    /**
4178     * Represents what McIDAS-V {@literal "knows"} about IDV actions.
4179     */
4180    protected enum ActionAttribute {
4181
4182        /**
4183         * Unique identifier for an IDV action. Required attribute.
4184         * 
4185         * @see IdvUIManager#ATTR_ID
4186         */
4187        ID(ATTR_ID), 
4188
4189        /**
4190         * Path to an icon for this action. Currently required. Note that 
4191         * McIDAS-V differs from the IDV in that actions must support different
4192         * icon sizes. This is implemented in McIDAS-V by simply having the value
4193         * of this path be a valid {@literal "format string"}, 
4194         * such as {@code image="/edu/wisc/ssec/mcidasv/resources/icons/toolbar/background-image%d.png"}
4195         * 
4196         * <p>The upshot is that this value <b>will not be a valid path in 
4197         * McIDAS-V</b>. Use either {@link IdvAction#getMenuIcon()} or 
4198         * {@link IdvAction#getIconForStyle}.
4199         * 
4200         * @see IdvUIManager#ATTR_IMAGE
4201         * @see IdvAction#getRawIconPath()
4202         * @see IdvAction#getMenuIcon()
4203         * @see IdvAction#getIconForStyle
4204         */
4205        ICON(ATTR_IMAGE), 
4206
4207        /**
4208         * Brief description of a IDV action. Required attribute.
4209         * @see IdvUIManager#ATTR_DESCRIPTION
4210         */
4211        DESCRIPTION(ATTR_DESCRIPTION), 
4212
4213        /**
4214         * Allows actions to be clustered into arbitrary groups. Currently 
4215         * optional; defaults to {@literal "General"}.
4216         * @see IdvUIManager#ATTR_GROUP
4217         */
4218        GROUP(ATTR_GROUP, "General"), 
4219
4220        /**
4221         * Actual method call used to invoke a given IDV action. Required 
4222         * attribute.
4223         * @see IdvUIManager#ATTR_ACTION
4224         */
4225        ACTION(ATTR_ACTION);
4226
4227        /**
4228         * A blank {@link String} if this is a required attribute, or a 
4229         * {@code String} value to use in case this attribute has not been 
4230         * specified by a given IDV action.
4231         */
4232        private final String defaultValue;
4233
4234        /**
4235         * String representation of this attribute as used by the IDV.
4236         * @see #asIdvString()
4237         */
4238        private final String idvString;
4239
4240        /** Whether or not this attribute is required. */
4241        private final boolean required;
4242
4243        /**
4244         * Creates a constant that represents a required IDV action attribute.
4245         * 
4246         * @param idvString Corresponding IDV attribute {@link String}. Cannot be {@code null}.
4247         * 
4248         * @throws NullPointerException if {@code idvString} is {@code null}.
4249         */
4250        ActionAttribute(final String idvString) {
4251            requireNonNull(idvString, "Cannot be associated with a null IDV action attribute String");
4252
4253            this.idvString = idvString; 
4254            this.defaultValue = ""; 
4255            this.required = true; 
4256        }
4257
4258        /**
4259         * Creates a constant that represents an optional IDV action attribute.
4260         * 
4261         * @param idvString Corresponding IDV attribute {@link String}. 
4262         * Cannot be {@code null}.
4263         * @param defValue Default value for actions that do not have this 
4264         * attribute. Cannot be {@code null} or an empty {@code String}.
4265         * 
4266         * @throws NullPointerException if either {@code idvString} or 
4267         * {@code defValue} is {@code null}.
4268         * @throws IllegalArgumentException if {@code defValue} is an empty 
4269         * {@code String}.
4270         * 
4271         */
4272        ActionAttribute(final String idvString, final String defValue) {
4273            requireNonNull(idvString, "Cannot be associated with a null IDV action attribute String");
4274            Contract.notNull(defValue, "Optional action attribute \"%s\" requires a non-null default value", toString());
4275            Contract.checkArg(!defValue.equals(""), "Optional action attribute \"%s\" requires something more descriptive than an empty String", toString());
4276
4277            this.idvString = idvString; 
4278            this.defaultValue = defValue; 
4279            this.required = (defaultValue.equals("")); 
4280        }
4281
4282        /**
4283         * @return The {@link String} representation of this attribute, as is 
4284         * used by the IDV.
4285         * 
4286         * @see IdvUIManager#ATTR_ACTION
4287         * @see IdvUIManager#ATTR_DESCRIPTION
4288         * @see IdvUIManager#ATTR_GROUP
4289         * @see IdvUIManager#ATTR_ID
4290         * @see IdvUIManager#ATTR_IMAGE
4291         */
4292        public String asIdvString() { return idvString; }
4293
4294        /**
4295         * @return {@literal "Default value"} for this attribute. 
4296         * Blank {@link String}s imply that the attribute is required (and 
4297         * thus lacks a true default value).
4298         */
4299        public String defaultValue() { return defaultValue; }
4300
4301        /**
4302         * @return Whether or not this attribute is a required attribute for 
4303         * valid {@link IdvAction}s.
4304         */
4305        public boolean isRequired() { return required; }
4306    }
4307
4308    /**
4309     * Represents the set of known {@link IdvAction IdvActions} in an idiom
4310     * that can be easily used by both the IDV and McIDAS-V.
4311     */
4312    // TODO(jon:101): use Sets instead of maps and whatnot
4313    // TODO(jon:103): create an invalid IdvAction
4314    public static final class IdvActions {
4315
4316        /** Maps {@literal "id"} values to {@link IdvAction IdvActions}. */
4317        private final Map<String, IdvAction> idToAction =
4318            new ConcurrentHashMap<>();
4319
4320        /**
4321         * Collects {@link IdvAction IdvActions} {@literal "under"} common
4322         * group values.
4323         */
4324        // TODO(jon:102): this should probably become concurrency-friendly.
4325        private final Map<String, Set<IdvAction>> groupToActions =
4326            new LinkedHashMap<>();
4327
4328        /**
4329         * Creates an object that represents the application's
4330         * {@link IdvAction IdvActions}.
4331         * 
4332         * @param idv Reference to the IDV {@literal "god"} object.
4333         *            Cannot be {@code null}.
4334         * @param collectionId IDV resource collection that contains our
4335         *                     actions. Cannot be {@code null}.
4336         * 
4337         * @throws NullPointerException if {@code idv} or {@code collectionId} 
4338         * is {@code null}. 
4339         */
4340        public IdvActions(final IntegratedDataViewer idv, final XmlIdvResource collectionId) {
4341            requireNonNull(idv, "Cannot provide a null IDV reference");
4342            requireNonNull(collectionId, "Cannot build actions from a null collection id");
4343
4344            // TODO(jon): benchmark use of xpath
4345            String query = "//action[@id and @image and @description and @action]";
4346            for (Element e : elements(idv, collectionId, query)) {
4347                IdvAction a = new IdvAction(e);
4348                String id = a.getAttribute(ActionAttribute.ID);
4349                idToAction.put(id, a);
4350                String group = a.getAttribute(ActionAttribute.GROUP);
4351                if (!groupToActions.containsKey(group)) {
4352                    groupToActions.put(group, new LinkedHashSet<>());
4353                }
4354                Set<IdvAction> groupedIds = groupToActions.get(group);
4355                groupedIds.add(a);
4356            }
4357        }
4358
4359        /**
4360         * Attempts to return the {@link IdvAction} associated with the given
4361         * {@code actionId}.
4362         * 
4363         * @param actionId Identifier to use in the search. Cannot be 
4364         * {@code null}.
4365         * 
4366         * @return Either the {@code IdvAction} that matches {@code actionId} 
4367         * or {@code null} if there was no match.
4368         * 
4369         * @throws NullPointerException if {@code actionId} is {@code null}.
4370         */
4371        // TODO(jon:103) here
4372        public IdvAction getAction(final String actionId) {
4373            requireNonNull(actionId, "Null action identifiers are not allowed");
4374            return idToAction.get(actionId);
4375        }
4376
4377        /**
4378         * Searches for the action associated with {@code actionId} and 
4379         * returns the value associated with the given {@link ActionAttribute}.
4380         * 
4381         * @param actionId Identifier to search for. Cannot be {@code null}.
4382         * @param attr Attribute whose value is desired. Cannot be {@code null}.
4383         * 
4384         * @return Either the desired attribute value of the desired action, 
4385         * or {@code null} if {@code actionId} has no associated action.
4386         * 
4387         * @throws NullPointerException if either {@code actionId} or 
4388         * {@code attr} is {@code null}.
4389         */
4390        // TODO(jon:103) here
4391        public String getAttributeForAction(final String actionId, final ActionAttribute attr) {
4392            requireNonNull(actionId, "Null action identifiers are not allowed");
4393            requireNonNull(attr, "Actions cannot have values associated with a null attribute");
4394            IdvAction action = idToAction.get(actionId);
4395            if (action == null) {
4396                return null;
4397            }
4398            return action.getAttribute(attr);
4399        }
4400
4401        /**
4402         * Attempts to return the XML {@link Element} that
4403         * {@literal "represents"} the action associated with {@code actionId}.
4404         * 
4405         * @param actionId Identifier whose XML element is desired.
4406         *                 Cannot be {@code null}.
4407         * 
4408         * @return Either the XML element associated with {@code actionId} or
4409         * {@code null}.
4410         * 
4411         * @throws NullPointerException if {@code actionId} is {@code null}.
4412         * 
4413         * @see IdvAction#originalElement
4414         */
4415        // TODO(jon:103) here
4416        public Element getElementForAction(final String actionId) {
4417            requireNonNull(actionId, "Cannot search for a null action identifier");
4418            IdvAction action = idToAction.get(actionId);
4419            if (action == null) {
4420                return null;
4421            }
4422            return action.getElement();
4423        }
4424
4425        /**
4426         * Attempts to return an {@link Icon} for a given {@link ActionAttribute#ID} and
4427         * {@link ToolbarStyle}.
4428         * 
4429         * @param actionId ID of the action whose {@literal "styled"} icon is 
4430         * desired. Cannot be {@code null}.
4431         * @param style Desired {@code Icon} style. Cannot be {@code null}.
4432         * 
4433         * @return Either the {@code Icon} associated with {@code actionId} 
4434         * and {@code style}, or {@code null}.
4435         * 
4436         * @throws NullPointerException if either {@code actionId} or 
4437         * {@code style} is {@code null}.
4438         */
4439        // TODO(jon:103) here
4440        public Icon getStyledIconFor(final String actionId, final ToolbarStyle style) {
4441            requireNonNull(actionId, "Cannot get an icon for a null action identifier");
4442            requireNonNull(style, "Cannot get an icon for a null ToolbarStyle");
4443            IdvAction a = idToAction.get(actionId);
4444            if (a == null) {
4445                return null;
4446            }
4447            return a.getIconForStyle(style);
4448        }
4449
4450        // TODO(jon:105): replace with something better
4451        public List<String> getAttributes(final ActionAttribute attr) {
4452            requireNonNull(attr, "Actions cannot have null attributes");
4453            List<String> attributeList = arrList(idToAction.size());
4454            for (Map.Entry<String, IdvAction> entry : idToAction.entrySet()) {
4455                attributeList.add(entry.getValue().getAttribute(attr));
4456            }
4457            return attributeList;
4458        }
4459
4460        /**
4461         * @return List of all known {@code IdvAction}s.
4462         */
4463        public List<IdvAction> getAllActions() {
4464            return arrList(idToAction.values());
4465        }
4466
4467        /**
4468         * @return List of all known action groupings.
4469         * 
4470         * @see ActionAttribute#GROUP
4471         * @see #getActionsForGroup(String)
4472         */
4473        public List<String> getAllGroups() {
4474            return arrList(groupToActions.keySet());
4475        }
4476
4477        /**
4478         * Returns the {@link Set} of {@link IdvAction}s associated with the 
4479         * given {@code group}.
4480         * 
4481         * @param group Group whose associated actions you want. Cannot be 
4482         * {@code null}.
4483         * 
4484         * @return Collection of {@code IdvAction}s associated with 
4485         * {@code group}. A blank collection is returned if there are no actions
4486         * associated with {@code group}.
4487         * 
4488         * @throws NullPointerException if {@code group} is {@code null}.
4489         * 
4490         * @see ActionAttribute#GROUP
4491         * @see #getAllGroups()
4492         */
4493        public Set<IdvAction> getActionsForGroup(final String group) {
4494            requireNonNull(group, "Actions cannot be associated with a null group");
4495            if (!groupToActions.containsKey(group)) {
4496                return Collections.emptySet();
4497            }
4498            return groupToActions.get(group);
4499        }
4500
4501        /**
4502         * Returns a summary of the known IDV actions. Please note that this 
4503         * format is subject to change, and is not intended for serialization.
4504         * 
4505         * @return String that looks like 
4506         * {@code [IdvActions@HASHCODE: actions=...]}.
4507         */
4508        @Override public String toString() {
4509            return String.format("[IdvActions@%x: actions=%s]", hashCode(), idToAction);
4510        }
4511    }
4512
4513    /**
4514     * Represents an individual IDV action. Should be fairly adaptable to
4515     * unforeseen changes from Unidata?
4516     */
4517    // TODO(jon:106): Implement equals/hashCode so that you can use these in Sets. The only relevant value should be the id, right?
4518    public static final class IdvAction {
4519
4520        /** The XML {@link Element} that represents this IDV action. */
4521        private final Element originalElement;
4522
4523        /** Mapping of (known) XML attributes to values for this individual action. */
4524        private final Map<ActionAttribute, String> attributes;
4525
4526        /** 
4527         * Simple {@literal "cache"} for the different icons this action has
4528         * displayed. This is {@literal "lazy"}, so the cache does not contain
4529         * icons for {@link ToolbarStyle}s that haven't been used. 
4530         */
4531        private final Map<ToolbarStyle, Icon> iconCache =
4532            new ConcurrentHashMap<>();
4533
4534        /**
4535         * Creates a representation of an IDV action using a given
4536         * {@link Element}.
4537         * 
4538         * @param element XML representation of an IDV action.
4539         *                Cannot be {@code null}.
4540         * 
4541         * @throws NullPointerException if {@code element} is {@code null}.
4542         * @throws IllegalArgumentException if {@code element} is not a valid
4543         * IDV action.
4544         * 
4545         * @see UIManager#isValidIdvAction(Element)
4546         */
4547        public IdvAction(final Element element) {
4548            requireNonNull(element, "Cannot build an action from a null element");
4549            // TODO(jon:107): need a way to diagnose what's wrong with the action?
4550            Contract.checkArg(isValidIdvAction(element), "Action lacks required attributes");
4551            originalElement = element;
4552            attributes = actionElementToMap(element);
4553        }
4554
4555        /**
4556         * @return Returns the {@literal "raw"} path to the icon associated 
4557         * with this action. Remember that this is actually a
4558         * {@literal "format string"} and should not be considered a valid path!
4559         * 
4560         * @see #getIconForStyle
4561         */
4562        public String getRawIconPath() {
4563            return attributes.get(ActionAttribute.ICON);
4564        }
4565
4566        /**
4567         * @return Returns the {@link Icon} associated with
4568         * {@link ToolbarStyle#SMALL}.
4569         */
4570        public Icon getMenuIcon() {
4571            return getIconForStyle(ToolbarStyle.SMALL);
4572        }
4573
4574        /**
4575         * Returns the {@link Icon} associated with this action and the given
4576         * {@link ToolbarStyle}.
4577         * 
4578         * @param style {@literal "Style"} of the {@code Icon} to be returned.
4579         * Cannot be {@code null}.
4580         * 
4581         * @return This action's {@code Icon} with {@code style}
4582         * {@literal "applied."}
4583         * 
4584         * @see ActionAttribute#ICON
4585         * @see #iconCache
4586         */
4587        public Icon getIconForStyle(final ToolbarStyle style) {
4588            requireNonNull(style, "Cannot build an icon for a null ToolbarStyle");
4589            if (!iconCache.containsKey(style)) {
4590                String styledPath = String.format(getRawIconPath(), style.getSize());
4591                URL tmp = getClass().getResource(styledPath);
4592                iconCache.put(style, new ImageIcon(Toolkit.getDefaultToolkit().getImage(tmp)));
4593            }
4594            return iconCache.get(style);
4595        }
4596
4597        /**
4598         * @return Returns the identifier of this {@code IdvAction}.
4599         */
4600        public String getId() {
4601            return getAttribute(ActionAttribute.ID);
4602        }
4603
4604        /**
4605         * Representation of this {@code IdvAction} as an
4606         * {@literal "IDV action call"}.
4607         * 
4608         * @return String that is suitable to hand off to the IDV for execution. 
4609         */
4610        public String getCommand() {
4611            return "idv.handleAction('action:"+getAttribute(ActionAttribute.ID)+"')";
4612        }
4613
4614        /**
4615         * Returns the value associated with a given {@link ActionAttribute} 
4616         * for this action.
4617         * 
4618         * @param attr ActionAttribute whose value you want.
4619         *             Cannot be {@code null}.
4620         * 
4621         * @return Value associated with {@code attr}.
4622         * 
4623         * @throws NullPointerException if {@code attr} is {@code null}.
4624         */
4625        public String getAttribute(final ActionAttribute attr) {
4626            requireNonNull(attr, "No values can be associated with a null ActionAttribute");
4627            return attributes.get(attr);
4628        }
4629
4630        /**
4631         * @return The XML {@link Element} used to create this {@code IdvAction}.
4632         */
4633        // TODO(jon:104): any way to copy this element? if so, this can become an immutable class!
4634        public Element getElement() {
4635            return originalElement;
4636        }
4637
4638        /**
4639         * Returns a brief description of this action. Please note that the 
4640         * format is subject to change and is not intended for serialization.
4641         * 
4642         * @return String that looks like
4643         * {@code [IdvAction@HASHCODE: attributes=...]}.
4644         */
4645        @Override public String toString() {
4646            return String.format("[IdvAction@%x: attributes=%s]", hashCode(), attributes);
4647        }
4648    }
4649}