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        mi.setToolTipText("Open Local Favorite Bundles Manager window");
1933
1934        final List bundles = getPersistenceManager().getBundles(bundleType);
1935        if (bundles.isEmpty()) {
1936            return;
1937        }
1938        final String title =
1939            getPersistenceManager().getBundleTitle(bundleType);
1940        final String bundleDir =
1941            getPersistenceManager().getBundleDirectory(bundleType);
1942
1943        JMenu bundleMenu = new JMenu(title);
1944        McVGuiUtils.setMenuImage(bundleMenu, Constants.ICON_FAVORITE_SMALL);
1945        bundleMenu.setMnemonic(GuiUtils.charToKeyCode(title));
1946
1947//        getPersistenceManager().initBundleMenu(bundleType, bundleMenu);
1948
1949        Hashtable catMenus = new Hashtable();
1950        inBundleMenu.addSeparator();
1951        inBundleMenu.add(bundleMenu);
1952        for (int i = 0; i < bundles.size(); i++) {
1953            SavedBundle bundle       = (SavedBundle) bundles.get(i);
1954            List        categories   = bundle.getCategories();
1955            JMenu       catMenu      = bundleMenu;
1956            String      mainCategory = "";
1957            for (int catIdx = 0; catIdx < categories.size(); catIdx++) {
1958                String category = (String) categories.get(catIdx);
1959                mainCategory += "." + category;
1960                JMenu tmpMenu = (JMenu) catMenus.get(mainCategory);
1961                if (tmpMenu == null) {
1962                    tmpMenu = new JMenu(category);
1963                    catMenu.add(tmpMenu);
1964                    catMenus.put(mainCategory, tmpMenu);
1965                }
1966                catMenu = tmpMenu;
1967            }
1968
1969            final SavedBundle theBundle = bundle;
1970            mi = new JMenuItem(bundle.getName());
1971            mi.addActionListener(new ActionListener() {
1972                public void actionPerformed(ActionEvent ae) {
1973                    //Do it in a thread
1974                    Misc.run(UIManager.this, "processBundle", theBundle);
1975                }
1976            });
1977            catMenu.add(mi);
1978        }
1979    }
1980
1981    /**
1982     * Overridden to build a custom Window menu.
1983     * @see ucar.unidata.idv.ui.IdvUIManager#makeWindowsMenu(JMenu, IdvWindow)
1984     */
1985    @Override public void makeWindowsMenu(final JMenu windowMenu, final IdvWindow idvWindow) {
1986        JMenuItem mi;
1987        boolean first = true;
1988
1989        mi = new JMenuItem("Show Data Explorer");
1990        McVGuiUtils.setMenuImage(mi, Constants.ICON_DATAEXPLORER_SMALL);
1991        mi.setToolTipText("Bring Data Explorer window to the front");
1992        mi.addActionListener(this);
1993        mi.setActionCommand(ACT_SHOW_DASHBOARD);
1994        windowMenu.add(mi);
1995
1996        makeTabNavigationMenu(windowMenu);
1997
1998        @SuppressWarnings("unchecked") // it's how the IDV does it.
1999        List windows = new ArrayList(IdvWindow.getWindows());
2000        for (int i = 0; i < windows.size(); i++) {
2001            final IdvWindow window = (IdvWindow)windows.get(i);
2002
2003            // Skip the main window
2004            if (window.getIsAMainWindow()) {
2005                continue;
2006            }
2007
2008            String title = window.getTitle();
2009            String titleParts[] = splitTitle(title);
2010
2011            if (titleParts.length == 2) {
2012                title = titleParts[1];
2013            }
2014
2015            // Skip the data explorer and display controller
2016            String dataSelectorNameParts[] = splitTitle(Constants.DATASELECTOR_NAME);
2017            if (title.equals(Constants.DATASELECTOR_NAME) || title.equals(dataSelectorNameParts[1])) {
2018                continue;
2019            }
2020
2021            // Add a meaningful name if there is none
2022            if (title.isEmpty()) {
2023                title = "<Unnamed>";
2024            }
2025
2026            if (window.isVisible()) {
2027                mi = new JMenuItem(title);
2028                mi.addActionListener(ae -> window.toFront());
2029
2030                if (first) {
2031                    windowMenu.addSeparator();
2032                    first = false;
2033                }
2034
2035                windowMenu.add(mi);
2036            }
2037        }
2038        Msg.translateTree(windowMenu);
2039    }
2040
2041    /**
2042     * Add tab navigation {@link JMenuItem JMenuItems} to the given 
2043     * {@code menu}.
2044     * 
2045     * @param menu Menu to which tab navigation menu items should be added. 
2046     *             Cannot be {@code null}.
2047     */
2048    private void makeTabNavigationMenu(final JMenu menu) {
2049        if (!didInitActions) {
2050            didInitActions = true;
2051            initTabNavActions();
2052        }
2053
2054        if (McVGuiUtils.getAllComponentHolders().size() <= 1) {
2055            return;
2056        }
2057
2058        menu.addSeparator();
2059
2060        menu.add(new JMenuItem(nextDisplayAction));
2061        menu.add(new JMenuItem(prevDisplayAction));
2062        menu.add(new JMenuItem(showDisplayAction));
2063
2064        if (!McVGuiUtils.getAllComponentGroups().isEmpty()) {
2065            menu.addSeparator();
2066        }
2067
2068        Msg.translateTree(menu);
2069    }
2070    
2071    /**
2072     * Add in the dynamic menu for displaying formulas
2073     *
2074     * @param menu edit menu to add to
2075     */
2076    public void makeFormulasMenu(JMenu menu) {
2077        MenuUtil.makeMenu(menu, getJythonManager().doMakeFormulaDataSourceMenuItems(null));
2078    }
2079    
2080    /** Whether or not the list of available actions has been initialized. */
2081    private boolean didInitActions = false;
2082
2083    /** Key combo for the popup with list of displays. */
2084    private ShowDisplayAction showDisplayAction;
2085
2086    /** 
2087     * Key combo for moving to the previous display relative to the current. For
2088     * key combos the lists of displays in the current window is circular.
2089     */
2090    private PrevDisplayAction prevDisplayAction;
2091
2092    /** 
2093     * Key combo for moving to the next display relative to the current. For
2094     * key combos the lists of displays in the current window is circular.
2095     */
2096    private NextDisplayAction nextDisplayAction;
2097
2098    /** Modifier key, like {@literal "control"} or {@literal "shift"}. */
2099    private static final String PROP_KB_MODIFIER = "mcidasv.tabbedui.display.kbmodifier";
2100
2101    /** Key that pops up the list of displays. Used in conjunction with {@code PROP_KB_MODIFIER}. */
2102    private static final String PROP_KB_SELECT_DISPLAY = "mcidasv.tabbedui.display.kbselect";
2103    
2104    /** Key for moving to the previous display. Used in conjunction with {@code PROP_KB_MODIFIER}. */
2105    private static final String PROP_KB_DISPLAY_PREV = "mcidasv.tabbedui.display.kbprev";
2106
2107    /** Key for moving to the next display. Used in conjunction with {@code PROP_KB_MODIFIER}. */
2108    private static final String PROP_KB_DISPLAY_NEXT = "mcidasv.tabbedui.display.kbnext";
2109
2110    /** Key for showing the dashboard. Used in conjunction with {@code PROP_KB_MODIFIER}. */
2111    private static final String PROP_KB_SHOW_DASHBOARD = "mcidasv.tabbedui.display.kbdashboard";
2112    
2113    /** Key for showing the Jython Shell. Used in conjunction with {@code PROP_KB_MODIFIER}. */
2114    private static final String PROP_KB_SHOW_SHELL = "mcidasv.tabbedui.display.kjythonshell";
2115    
2116    /** Key for showing the Jython Library (modifier is {@literal "alt"} key). */
2117    private static final String PROP_KB_SHOW_LIBRARY = "mcidasv.tabbedui.display.kjythonlibrary";
2118
2119    // TODO: make all this stuff static: mod + acc don't need to read the properties file.
2120    // look at: http://community.livejournal.com/jkff_en/341.html
2121    // look at: effective java, particularly the stuff about enums
2122    private void initTabNavActions() {
2123        String mod = idv.getProperty(PROP_KB_MODIFIER, "control") + " ";
2124        String acc = idv.getProperty(PROP_KB_SELECT_DISPLAY, "L");
2125
2126        String stroke = mod + acc;
2127        showDisplayAction = new ShowDisplayAction(KeyStroke.getKeyStroke(stroke));
2128
2129        acc = idv.getProperty(PROP_KB_DISPLAY_PREV, "P");
2130        stroke = mod + acc;
2131        prevDisplayAction = new PrevDisplayAction(KeyStroke.getKeyStroke(stroke));
2132
2133        acc = idv.getProperty(PROP_KB_DISPLAY_NEXT, "N");
2134        stroke = mod + acc;
2135        nextDisplayAction = new NextDisplayAction(KeyStroke.getKeyStroke(stroke));
2136    }
2137
2138    /**
2139     * Add all the show window keyboard shortcuts. To make keyboard shortcuts
2140     * global, i.e., available no matter what window is active, the appropriate 
2141     * actions have to be added the the window contents action and input maps.
2142     * 
2143     * FIXME: This can't be the right way to do this!
2144     * 
2145     * @param window IdvWindow that requires keyboard shortcut capability.
2146     */
2147    private void initDisplayShortcuts(IdvWindow window) {
2148        //mjh aug2014 make sure showDisplayAction etc. are initialized:
2149        initTabNavActions();
2150        didInitActions = true;
2151
2152        JComponent jcomp = window.getContents();
2153        jcomp.getActionMap().put("show_disp", showDisplayAction);
2154        jcomp.getActionMap().put("prev_disp", prevDisplayAction);
2155        jcomp.getActionMap().put("next_disp", nextDisplayAction);
2156        jcomp.getActionMap().put("show_dashboard", new AbstractAction() {
2157            private static final long serialVersionUID = -364947940824325949L;
2158            public void actionPerformed(ActionEvent evt) {
2159                showDashboard();
2160            }
2161        });
2162        jcomp.getActionMap().put("show_jython_shell", new AbstractAction() {
2163            @Override public void actionPerformed(ActionEvent e) {
2164                getJythonManager().createShell();
2165            }
2166        });
2167        jcomp.getActionMap().put("show_jython_library", new AbstractAction() {
2168            @Override public void actionPerformed(ActionEvent e) {
2169                getJythonManager().showJythonEditor();
2170            }
2171        });
2172
2173        String mod = getIdv().getProperty(PROP_KB_MODIFIER, "control");
2174        String acc = getIdv().getProperty(PROP_KB_SELECT_DISPLAY, "L");
2175        jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2176            KeyStroke.getKeyStroke(mod + ' ' + acc),
2177            "show_disp"
2178        );
2179
2180        acc = getIdv().getProperty(PROP_KB_SHOW_DASHBOARD, "MINUS");
2181        jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2182            KeyStroke.getKeyStroke(mod + ' ' + acc),
2183            "show_dashboard"
2184        );
2185
2186        acc = getIdv().getProperty(PROP_KB_DISPLAY_NEXT, "N");
2187        jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2188            KeyStroke.getKeyStroke(mod + ' ' + acc),
2189            "next_disp"
2190        );
2191
2192        acc = getIdv().getProperty(PROP_KB_DISPLAY_PREV, "P");
2193        jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2194            KeyStroke.getKeyStroke(mod + ' ' + acc),
2195            "prev_disp"
2196        );
2197        
2198        acc = getIdv().getProperty(PROP_KB_SHOW_SHELL, "J");
2199        jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2200            KeyStroke.getKeyStroke(mod + ' ' + acc),
2201            "show_jython_shell");
2202            
2203        mod = "alt";
2204        acc = getIdv().getProperty(PROP_KB_SHOW_LIBRARY, "J");
2205        jcomp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
2206            KeyStroke.getKeyStroke(mod + ' ' + acc),
2207            "show_jython_library");
2208    }
2209
2210    /**
2211     * Show Bruce's display selector widget.
2212     */
2213    protected void showDisplaySelector() {
2214        IdvWindow mainWindow = IdvWindow.getActiveWindow();
2215        JPanel contents = new JPanel();
2216        contents.setLayout(new BorderLayout());
2217        JComponent comp = getDisplaySelectorComponent();
2218        final JDialog dialog = new JDialog(mainWindow.getFrame(), "List Displays", true);
2219        dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
2220        contents.add(comp, BorderLayout.CENTER);
2221        JButton button = new JButton("OK");
2222        button.addActionListener(new ActionListener() {
2223            public void actionPerformed(ActionEvent evt) {
2224                final ViewManager vm = getVMManager().getLastActiveViewManager();
2225                // final DisplayProps disp = getDisplayProps(vm);
2226                // if (disp != null)
2227                //    showDisplay(disp);
2228                final McvComponentHolder holder = (McvComponentHolder)getViewManagerHolder(vm);
2229                if (holder != null) {
2230                    holder.setAsActiveTab();
2231                }
2232
2233                // have to do this on the event dispatch thread so we make
2234                // sure it happens after showDisplay
2235                SwingUtilities.invokeLater(new Runnable() {
2236                    public void run() {
2237                        //setActiveDisplay(disp, disp.managers.indexOf(vm));
2238                        if (holder != null) {
2239                            getVMManager().setLastActiveViewManager(vm);
2240                        }
2241                    }
2242                });
2243
2244                dialog.dispose();
2245            }
2246        });
2247        JPanel buttonPanel = new JPanel();
2248        buttonPanel.add(button);
2249        dialog.add(buttonPanel, BorderLayout.AFTER_LAST_LINE);
2250        JScrollPane scroller = new JScrollPane(contents);
2251        scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
2252        scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
2253        dialog.add(scroller, BorderLayout.CENTER);
2254        dialog.setSize(200, 300);
2255        dialog.setLocationRelativeTo(mainWindow.getFrame());
2256        dialog.setVisible(true);
2257    }
2258
2259    private class ShowDisplayAction extends AbstractAction {
2260        private static final long serialVersionUID = -4609753725057124244L;
2261        private static final String ACTION_NAME = "List Displays...";
2262        public ShowDisplayAction(KeyStroke k) {
2263            super(ACTION_NAME);
2264            putValue(Action.ACCELERATOR_KEY, k);
2265        }
2266
2267        public void actionPerformed(ActionEvent e) {
2268            showDisplaySelector();
2269        }
2270    }
2271
2272    private class PrevDisplayAction extends AbstractAction {
2273        private static final long serialVersionUID = -3551890663976755671L;
2274        private static final String ACTION_NAME = "Previous Display";
2275
2276        public PrevDisplayAction(KeyStroke k) {
2277            super(ACTION_NAME);
2278            putValue(Action.ACCELERATOR_KEY, k);
2279        }
2280
2281        public void actionPerformed(ActionEvent e) {
2282            McvComponentHolder prev = (McvComponentHolder)McVGuiUtils.getBeforeActiveHolder();
2283            if (prev != null) {
2284                prev.setAsActiveTab();
2285            }
2286        }
2287    }
2288
2289    private class NextDisplayAction extends AbstractAction {
2290        private static final long serialVersionUID = 5431901451767117558L;
2291        private static final String ACTION_NAME = "Next Display";
2292
2293        public NextDisplayAction(KeyStroke k) {
2294            super(ACTION_NAME);
2295            putValue(Action.ACCELERATOR_KEY, k);
2296        }
2297
2298        public void actionPerformed(ActionEvent e) {
2299            McvComponentHolder next = (McvComponentHolder)McVGuiUtils.getAfterActiveHolder();
2300            if (next != null) {
2301                next.setAsActiveTab();
2302            }
2303        }
2304    }
2305
2306    /**
2307     * Populate a "new display" menu from the available skin list. Many thanks
2308     * to Bruce for doing this in the venerable TabbedUIManager.
2309     * 
2310     * @param newDisplayMenu menu to populate.
2311     * @param inWindow Is the skinned display to be created in a window?
2312     * 
2313     * @see IdvResourceManager#RSC_SKIN
2314     * 
2315     * @return Menu item populated with display skins
2316     */
2317    protected JMenuItem doMakeNewDisplayMenu(JMenuItem newDisplayMenu, 
2318        final boolean inWindow) 
2319    {
2320        if (newDisplayMenu != null) {
2321
2322            String skinFilter = "idv.skin";
2323            if (!inWindow) {
2324                skinFilter = "mcv.skin";
2325            }
2326
2327            final XmlResourceCollection skins =
2328                getResourceManager().getXmlResources(
2329                    IdvResourceManager.RSC_SKIN);
2330
2331            Map<String, JMenu> menus = new Hashtable<>();
2332            for (int i = 0; i < skins.size(); i++) {
2333                final Element root = skins.getRoot(i);
2334                if (root == null) {
2335                    continue;
2336                }
2337
2338                // filter out mcv or idv skins based on whether or not we're
2339                // interested in tabs or new windows.
2340                final String skinid = skins.getProperty("skinid", i);
2341                if ((skinid != null) && skinid.startsWith(skinFilter)) {
2342                    continue;
2343                }
2344
2345                final int skinIndex = i;
2346                List<String> names =
2347                    StringUtil.split(skins.getShortName(i), ">", true, true);
2348
2349                JMenuItem theMenu = newDisplayMenu;
2350                String path = "";
2351                for (int nameIdx = 0; nameIdx < names.size() - 1; nameIdx++) {
2352                    String catName = names.get(nameIdx);
2353                    path = path + '>' + catName;
2354                    JMenu tmpMenu = menus.get(path);
2355                    if (tmpMenu == null) {
2356                        tmpMenu = new JMenu(catName);
2357                        theMenu.add(tmpMenu);
2358                        menus.put(path, tmpMenu);
2359                    }
2360                    theMenu = tmpMenu;
2361                }
2362
2363                final String name = names.get(names.size() - 1);
2364
2365                IdvWindow window = IdvWindow.getActiveWindow();
2366                for (final McvComponentGroup group : McVGuiUtils.idvGroupsToMcv(window)) {
2367                    JMenuItem mi = new JMenuItem(name);
2368
2369                    mi.addActionListener(ae -> {
2370                        if (!inWindow) {
2371                            createNewTab(skinid);
2372                        } else {
2373                            createNewWindow(null, true,
2374                                getStateManager().getTitle(), skins.get(
2375                                    skinIndex).toString(), skins.getRoot(
2376                                    skinIndex, false), inWindow, null);
2377                        }
2378                    });
2379                    theMenu.add(mi);
2380                }
2381            }
2382
2383            // attach the dynamic skin menu item to the tab menu.
2384//            if (!inWindow) {
2385//                ((JMenu)newDisplayMenu).addSeparator();
2386//                IdvWindow window = IdvWindow.getActiveWindow();
2387//
2388//                final McvComponentGroup group =
2389//                    (McvComponentGroup)window.getComponentGroups().get(0);
2390//
2391//                JMenuItem mi = new JMenuItem("Choose Your Own Adventure...");
2392//                mi.addActionListener(new ActionListener() {
2393//
2394//                    public void actionPerformed(ActionEvent e) {
2395//                        makeDynamicSkin(group);
2396//                    }
2397//                });
2398//                newDisplayMenu.add(mi);
2399//            }
2400        }
2401        return newDisplayMenu;
2402    }
2403
2404    // for the time being just create some basic viewmanagers.
2405//    public void makeDynamicSkin(McvComponentGroup group) {
2406//        // 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...)
2407//        try {
2408//            Document doc = XmlUtil.getDocument(SKIN_TEMPLATE);
2409//            Element root = doc.getDocumentElement();
2410//            Element rightChild = doc.createElement("idv.view");
2411//            rightChild.setAttribute("class", "ucar.unidata.idv.TransectViewManager");
2412//            rightChild.setAttribute("viewid", "viewright1337");
2413//            rightChild.setAttribute("id", "viewright");
2414//            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%;");
2415//
2416//            Element leftChild = doc.createElement("idv.view");
2417//            leftChild.setAttribute("class", "ucar.unidata.idv.MapViewManager");
2418//            leftChild.setAttribute("viewid", "viewleft1337");
2419//            leftChild.setAttribute("id", "viewleft");
2420//            leftChild.setAttribute("properties", "name=Panel 2;clickToFocus=true;showToolBars=true;shareViews=true;showControlLegend=false;size=300:400;shareGroup=view%versionuid%;");
2421//
2422//            Element startNode = XmlUtil.findElement(root, "splitpane", "embeddednode", "true");
2423//            startNode.appendChild(rightChild);
2424//            startNode.appendChild(leftChild);
2425//            group.makeDynamicSkin(root);
2426//        } catch (Exception e) {
2427//            LogUtil.logException("Error: parsing skin template:", e);
2428//        }
2429//    }
2430//
2431//    private static final String SKIN_TEMPLATE = 
2432//        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
2433//        "<skin embedded=\"true\">\n" +
2434//        "  <ui>\n" +
2435//        "    <panel layout=\"border\" bgcolor=\"red\">\n" +
2436//        "      <idv.menubar place=\"North\"/>\n" +
2437//        "      <panel layout=\"border\" place=\"Center\">\n" +
2438//        "        <panel layout=\"flow\" place=\"North\">\n" +
2439//        "          <idv.toolbar id=\"idv.toolbar\" place=\"West\"/>\n" +
2440//        "          <panel id=\"idv.favoritesbar\" place=\"North\"/>\n" +
2441//        "        </panel>\n" +
2442//        "        <splitpane embeddednode=\"true\" resizeweight=\"0.5\" onetouchexpandable=\"true\" orientation=\"h\" bgcolor=\"blue\" layout=\"grid\" cols=\"2\" place=\"Center\">\n" +
2443//        "        </splitpane>\n" +
2444//        "      </panel>\n" +
2445//        "      <component idref=\"bottom_bar\"/>\n" +
2446//        "    </panel>\n" +
2447//        "  </ui>\n" +
2448//        "  <styles>\n" +
2449//        "    <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" +
2450//        "    <style class=\"textbtn\" space=\"2\" mouse_enter=\"ui.setText(idv.messagelabel,prop:tooltip)\" mouse_exit=\"ui.setText(idv.messagelabel,)\"/>\n" +
2451//        "  </styles>\n" +
2452//        "  <components>\n" +
2453//        "    <idv.statusbar place=\"South\" id=\"bottom_bar\"/>\n" +
2454//        "  </components>\n" +
2455//        "  <properties>\n" +
2456//        "    <property name=\"icon.wait.wait\" value=\"/ucar/unidata/idv/images/wait.gif\"/>\n" +
2457//        "  </properties>\n" +
2458//        "</skin>\n";
2459
2460    private int holderCount;
2461    
2462    /**
2463     * Associates a given ViewManager with a given ComponentHolder.
2464     * 
2465     * @param vm The ViewManager that is inside {@code holder}.
2466     * @param holder The ComponentHolder that contains {@code vm}.
2467     */
2468    public void setViewManagerHolder(ViewManager vm, ComponentHolder holder) {
2469        viewManagers.put(vm, holder);
2470        holderCount = getComponentHolders().size();
2471    }
2472
2473    public Set<ComponentHolder> getComponentHolders() {
2474        return newHashSet(viewManagers.values());
2475    }
2476
2477    public int getComponentHolderCount() {
2478        return holderCount;
2479    }
2480
2481    public int getComponentGroupCount() {
2482        return getComponentGroups().size();
2483    }
2484
2485    /**
2486     * Returns the ComponentHolder containing the given ViewManager.
2487     * 
2488     * @param vm The ViewManager whose ComponentHolder is needed.
2489     * 
2490     * @return Either {@code null} or the {@code ComponentHolder}.
2491     */
2492    public ComponentHolder getViewManagerHolder(ViewManager vm) {
2493        return viewManagers.get(vm);
2494    }
2495
2496    /**
2497     * Disassociate a given {@code ViewManager} from its 
2498     * {@code ComponentHolder}.
2499     * 
2500     * @param vm {@code ViewManager} to disassociate.
2501     * 
2502     * @return The associated {@code ComponentHolder}.
2503     */
2504    public ComponentHolder removeViewManagerHolder(ViewManager vm) {
2505        ComponentHolder holder = viewManagers.remove(vm);
2506        holderCount = getComponentHolders().size();
2507        return holder;
2508    }
2509
2510    /**
2511     * Overridden to keep the dashboard around after it's initially created.
2512     * Also give the user the ability to show a particular tab.
2513     * 
2514     * @see ucar.unidata.idv.ui.IdvUIManager#showDashboard()
2515     */
2516    @Override public void showDashboard() {
2517        showDashboard("");
2518    }
2519
2520    /**
2521     * Creates the {@link McIDASVViewPanel} component that shows up in the 
2522     * dashboard.
2523     * 
2524     * @return McIDAS-V specific view panel.
2525     */
2526    @Override protected ViewPanel doMakeViewPanel() {
2527        ViewPanel vp = new McIDASVViewPanel(idv);
2528        vp.getContents();
2529        return vp;
2530    }
2531
2532    /**
2533     * Build a mapping of {@literal "skin"} IDs to their indicies within skin
2534     * resources.
2535     * 
2536     * @return Map of skin ids to their index within the skin resource.
2537     */
2538    private Map<String, Integer> readSkinIds() {
2539        XmlResourceCollection skins = 
2540            getResourceManager().getXmlResources(IdvResourceManager.RSC_SKIN);
2541        Map<String, Integer> ids = new HashMap<>(skins.size());
2542        for (int i = 0; i < skins.size(); i++) {
2543            String id = skins.getProperty("skinid", i);
2544            if (id != null) {
2545                ids.put(id, i);
2546            }
2547        }
2548        return ids;
2549    }
2550
2551    /**
2552     * Adds a skinned component holder to the active component group.
2553     * 
2554     * @param skinId The value of the skin's skinid attribute.
2555     */
2556    public void createNewTab(final String skinId) {
2557        IdvWindow activeWindow = IdvWindow.getActiveWindow();
2558        IdvComponentGroup group =
2559            McVGuiUtils.getComponentGroup(activeWindow);
2560        if (skinIds.containsKey(skinId)) {
2561            group.makeSkin(skinIds.get(skinId));
2562        }
2563        JFrame frame = activeWindow.getFrame();
2564        if (frame != null) {
2565            frame.setPreferredSize(frame.getSize());
2566        }
2567    }
2568
2569    /**
2570     * Get the list of  initial skins to create windows for
2571     *
2572     * @return List of UI skins to create
2573     */
2574    private List<String> getInitialSkins() {
2575        //Run the skins through the getResourcePath in case there are any %USERPATH% macros
2576        List<String> temp = StringUtil.split(
2577                getStateManager().getProperty("idv.ui.initskins", ""), ";", true,
2578                true);
2579        List<String> skins = new ArrayList<String>();
2580        for(String skin: temp) {
2581            skins.add(getResourceManager().getResourcePath(skin));
2582        }
2583        return skins;
2584    }
2585
2586    /**
2587     * Create the basic windows. This gets called at start up and if the user
2588     * presses "show dashboard" and there isn't any windows available
2589     */
2590    @Override public void doMakeBasicWindows() {
2591        splashMsg("Creating User Interface");
2592        List skins = getInitialSkins();
2593        for (int i = 0; i < skins.size(); i++) {
2594            String skin = (String) skins.get(i);
2595            try {
2596                createNewWindow(new ArrayList(), skin);
2597            } catch (Throwable exc) {
2598                logException("Creating UI from skin:" + skin, exc);
2599            }
2600        }
2601    }
2602
2603    /**
2604     * Method to do the work of showing the Data Explorer (nee Dashboard).
2605     * 
2606     * @param tabName Name of the tab that should be made active. 
2607     *                Cannot be {@code null}, but empty {@code String} values 
2608     *                will not change the active tab.
2609     */
2610    @SuppressWarnings("unchecked") // IdvWindow.getWindows only adds IdvWindows.
2611    public void showDashboard(String tabName) {
2612        if (!initDone) {
2613            return;
2614        } else if (dashboard == null) {
2615            showWaitCursor();
2616            doMakeBasicWindows();
2617            showNormalCursor();
2618            String title = makeTitle(getStateManager().getTitle(), Constants.DATASELECTOR_NAME);
2619            for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) {
2620                if (title.equals(window.getTitle())) {
2621                    dashboard = window;
2622                    dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
2623                }
2624            }
2625        } else {
2626            dashboard.show();
2627        }
2628
2629        if (tabName.isEmpty()) {
2630            return;
2631        }
2632
2633        // Dig two panels deep looking for a JTabbedPane
2634        // If you find one, try to show the requested tab name
2635        JComponent contents = dashboard.getContents();
2636        JComponent component = (JComponent)contents.getComponent(0);
2637        JTabbedPane tPane = null;
2638        if (component instanceof JTabbedPane) {
2639            tPane = (JTabbedPane)component;
2640        }
2641        else {
2642            JComponent component2 = (JComponent)component.getComponent(0);
2643            if (component2 instanceof JTabbedPane) {
2644                tPane = (JTabbedPane)component2;
2645            }
2646        }
2647        if (tPane != null) {
2648            for (int i=0; i<tPane.getTabCount(); i++) {
2649                if (tabName.equals(tPane.getTitleAt(i))) {
2650                    tPane.setSelectedIndex(i);
2651                    break;
2652                }
2653            }
2654        }
2655    }
2656
2657    /**
2658     * Search through list of {@link IdvWindow IdvWindows} and return the {@literal "Data Explorer"},
2659     * if it exists.
2660     *
2661     * @return {@literal "Data Explorer"} window or {@code null} if it does not exist.
2662     */
2663    public IdvWindow getDashboardWindow() {
2664        IdvWindow dash = null;
2665        List<IdvWindow> windows = cast(IdvWindow.getWindows());
2666        for (IdvWindow window : windows) {
2667            if (Constants.DATASELECTOR_NAME.equals(window.getTitle())) {
2668                dash = window;
2669                break;
2670            }
2671        }
2672        return dash;
2673    }
2674
2675    /**
2676     * Show the support request form
2677     *
2678     * @param description Default value for the description form entry
2679     * @param stackTrace The stack trace that caused this error.
2680     * @param dialog The dialog to put the gui in, if non-null.
2681     */
2682    public void showSupportForm(final String description, 
2683        final String stackTrace, final JDialog dialog) 
2684    {
2685        java.awt.EventQueue.invokeLater(() -> {
2686            // TODO: mcvstatecollector should have a way to gather the
2687            // exception information..
2688            McIDASV mcv = (McIDASV)getIdv();
2689            new SupportForm(getStore(), new McvStateCollector(mcv)).setVisible(true);
2690        });
2691    }
2692
2693    /**
2694     * Attempts to locate and display a dashboard component using an ID.
2695     * 
2696     * @param id ID of the desired component.
2697     * 
2698     * @return True if {@code id} corresponds to a component. False otherwise.
2699     */
2700    public boolean showDashboardComponent(String id) {
2701        Object comp = findComponent(id);
2702        if (comp != null) {
2703            GuiUtils.showComponentInTabs((JComponent)comp);
2704            return true;
2705        } else {
2706            super.showDashboard();
2707            for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) {
2708                String title = makeTitle(
2709                    getStateManager().getTitle(),
2710                    Constants.DATASELECTOR_NAME
2711                );
2712                if (title.equals(window.getTitle())) {
2713                    dashboard = window;
2714                    dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
2715                }
2716            }
2717        }
2718        return false;
2719    }
2720
2721    /**
2722     * Close and dispose of the splash window (if it has been created).
2723     */
2724    @Override
2725    public void splashClose() {
2726        if (splash != null) {
2727            splash.doClose();
2728        }
2729    }
2730
2731    /**
2732     * Show a message in the splash screen (if it exists)
2733     *
2734     * @param m The message to show
2735     */
2736    @Override public void splashMsg(String m) {
2737        if (splash != null) {
2738            splash.splashMsg(m);
2739        }
2740    }
2741
2742    /**
2743     * Uses a given toolbar editor to repopulate all toolbars so that they 
2744     * correspond to the user's choice of actions.
2745     * 
2746     * @param tbe The toolbar editor that contains the actions the user wants.
2747     */
2748    public void setCurrentToolbars(final McvToolbarEditor tbe) {
2749        List<TwoFacedObject> tfos = tbe.getTLP().getCurrentEntries();
2750        List<String> buttonIds = new ArrayList<>(tfos.size());
2751        for (TwoFacedObject tfo : tfos) {
2752            if (McvToolbarEditor.isSpace(tfo)) {
2753                buttonIds.add(null);
2754            } else {
2755                buttonIds.add(TwoFacedObject.getIdString(tfo));
2756            }
2757        }
2758
2759        cachedButtons = buttonIds;
2760
2761        for (JToolBar toolbar : toolbars) {
2762            toolbar.setVisible(false);
2763            populateToolbar(toolbar);
2764            toolbar.setVisible(true);
2765        }
2766    }
2767
2768    /**
2769     * Append a string and object to the buffer
2770     *
2771     * @param sb  StringBuffer to append to
2772     * @param name  Name of the object
2773     * @param value  the object value
2774     */
2775    private void append(StringBuffer sb, String name, Object value) {
2776        sb.append("<b>").append(name).append("</b>: ").append(value).append("<br>");
2777    }
2778
2779    private JMenuItem makeControlDescriptorItem(ControlDescriptor cd) {
2780        JMenuItem mi = new JMenuItem();
2781        if (cd != null) {
2782            mi = new JMenuItem(cd.getLabel());
2783            mi.addActionListener(new ObjectListener(cd) {
2784                public void actionPerformed(ActionEvent ev) {
2785                    idv.doMakeControl(new ArrayList(),
2786                        (ControlDescriptor)theObject);
2787                }
2788            });
2789        }
2790        return mi;
2791    }
2792
2793    /* (non-javadoc)
2794     * Overridden so that the toolbar will update upon saving a bundle.
2795     */
2796    @Override public void displayTemplatesChanged() {
2797        super.displayTemplatesChanged();
2798        for (JToolBar toolbar : toolbars) {
2799            toolbar.setVisible(false);
2800            populateToolbar(toolbar);
2801            toolbar.setVisible(true);
2802        }
2803    }
2804
2805    /**
2806     * Called when there has been any change to the favorite bundles and is
2807     * most useful for triggering an update to the {@literal "toolbar bundles"}.
2808     */
2809    @Override public void favoriteBundlesChanged() {
2810        SwingUtilities.invokeLater(() -> {
2811            for (JToolBar toolbar : toolbars) {
2812                toolbar.setVisible(false);
2813                populateToolbar(toolbar);
2814                toolbar.setVisible(true);
2815            }
2816        });
2817    }
2818
2819    /**
2820     * Show the support request form in a non-swing thread. We do this because we cannot
2821     * call the HttpFormEntry.showUI from a swing thread
2822     *
2823     * @param description Default value for the description form entry
2824     * @param stackTrace The stack trace that caused this error.
2825     * @param dialog The dialog to put the gui in, if non-null.
2826     */
2827
2828    private void showSupportFormInThread(String description,
2829                                         String stackTrace, JDialog dialog) {
2830        List<HttpFormEntry> entries = new ArrayList<>();
2831
2832        StringBuffer extra   = new StringBuffer("<h3>McIDAS-V</h3>\n");
2833        Hashtable<String, String> table = 
2834            ((StateManager)getStateManager()).getVersionInfo();
2835        append(extra, "mcv.version.general", table.get("mcv.version.general"));
2836        append(extra, "mcv.version.build", table.get("mcv.version.build"));
2837        append(extra, "idv.version.general", table.get("idv.version.general"));
2838        append(extra, "idv.version.build", table.get("idv.version.build"));
2839
2840        extra.append("<h3>OS</h3>\n");
2841        append(extra, "os.name", System.getProperty("os.name"));
2842        append(extra, "os.arch", System.getProperty("os.arch"));
2843        append(extra, "os.version", System.getProperty("os.version"));
2844
2845        extra.append("<h3>Java</h3>\n");
2846        append(extra, "java.vendor", System.getProperty("java.vendor"));
2847        append(extra, "java.version", System.getProperty("java.version"));
2848        append(extra, "java.home", System.getProperty("java.home"));
2849
2850        StringBuffer javaInfo = new StringBuffer();
2851        javaInfo.append("Java: home: " + System.getProperty("java.home"));
2852        javaInfo.append(" version: " + System.getProperty("java.version"));
2853
2854        Class c = null;
2855        try {
2856            c = Class.forName("javax.media.j3d.VirtualUniverse");
2857            Method method = Misc.findMethod(c, "getProperties",
2858                                            new Class[] {});
2859            if (method == null) {
2860                javaInfo.append("j3d <1.3");
2861            } else {
2862                try {
2863                    Map m = (Map)method.invoke(c, new Object[] {});
2864                    javaInfo.append(" j3d:" + m.get("j3d.version"));
2865                    append(extra, "j3d.version", m.get("j3d.version"));
2866                    append(extra, "j3d.vendor", m.get("j3d.vendor"));
2867                    append(extra, "j3d.renderer", m.get("j3d.renderer"));
2868                } catch (Exception exc) {
2869                    javaInfo.append(" j3d:" + "unknown");
2870                }
2871            }
2872        } catch (ClassNotFoundException exc) {
2873            append(extra, "j3d", "none");
2874        }
2875
2876        boolean persistCC = getStore().get("mcv.supportreq.cc", true);
2877
2878        JCheckBox ccMyself = new JCheckBox("Send Copy of Support Request to Me", persistCC);
2879        ccMyself.addActionListener(e -> {
2880            JCheckBox cb = (JCheckBox)e.getSource();
2881            getStore().put("mcv.supportreq.cc", cb.isSelected());
2882        });
2883
2884        boolean doWrap = idv.getProperty(PROP_WRAP_SUPPORT_DESC, true);
2885
2886        HttpFormEntry descriptionEntry;
2887        HttpFormEntry nameEntry;
2888        HttpFormEntry emailEntry;
2889        HttpFormEntry orgEntry;
2890
2891        entries.add(nameEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2892                "form_data[fromName]", "Name:",
2893                getStore().get(PROP_HELP_NAME, (String) null)));
2894        entries.add(emailEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2895                "form_data[email]", "Your Email:",
2896                getStore().get(PROP_HELP_EMAIL, (String) null)));
2897        entries.add(orgEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2898                "form_data[organization]", "Organization:",
2899                getStore().get(PROP_HELP_ORG, (String) null)));
2900        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2901                                      "form_data[subject]", "Subject:"));
2902
2903        entries.add(
2904            new HttpFormEntry(
2905                HttpFormEntry.TYPE_LABEL, "",
2906                "<html>Please provide a <i>thorough</i> description of the problem you encountered:</html>"));
2907        entries.add(descriptionEntry =
2908            new FormEntry(doWrap, HttpFormEntry.TYPE_AREA,
2909                              "form_data[description]", "Description:",
2910                              description, 5, 30, true));
2911
2912        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE,
2913                                      "form_data[att_two]", "Attachment 1:", "",
2914                                      false));
2915        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE,
2916                                      "form_data[att_three]", "Attachment 2:", "",
2917                                      false));
2918
2919        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2920                                      "form_data[submit]", "", "Send Email"));
2921        
2922        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2923                                      "form_data[p_version]", "",
2924                                      getStateManager().getVersion()
2925                                      + " build date:"
2926                                      + getStateManager().getBuildDate()));
2927        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2928                                      "form_data[opsys]", "",
2929                                      System.getProperty("os.name")));
2930        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2931                                      "form_data[hardware]", "",
2932                                      javaInfo.toString()));
2933        
2934        JLabel topLabel = 
2935            new JLabel("<html>This form allows you to send a support request to the McIDAS Help Desk.<br></html>");
2936
2937        JCheckBox includeBundleCbx =
2938            new JCheckBox("Include Current State as Bundle", false);
2939
2940        List<JCheckBox> checkboxes = list(includeBundleCbx, ccMyself);
2941
2942        boolean alreadyHaveDialog = true;
2943        if (dialog == null) {
2944            // NOTE: if the dialog is modeless you can leave alreadyHaveDialog
2945            // alone. If the dialog is modal you need to set alreadyHaveDialog
2946            // to false.
2947            // If alreadyHaveDialog is false with a modeless dialog, the later
2948            // call to HttpFormEntry.showUI will return false and break out of
2949            // the while loop without talking to the HTTP server.
2950            dialog = GuiUtils.createDialog(LogUtil.getCurrentWindow(),
2951                                           "Support Request Form", false);
2952//            alreadyHaveDialog = false;
2953        }
2954
2955        JLabel statusLabel = GuiUtils.cLabel(" ");
2956        JComponent bottom = LayoutUtil.vbox(LayoutUtil.leftVbox(checkboxes), statusLabel);
2957
2958        while (true) {
2959            //Show form. Check if user pressed cancel.
2960            statusLabel.setText(" ");
2961            if ( !HttpFormEntry.showUI(entries, LayoutUtil.inset(topLabel, 10),
2962                                       bottom, dialog, alreadyHaveDialog)) {
2963                break;
2964            }
2965            statusLabel.setText("Posting support request...");
2966
2967            //Save persistent state
2968            getStore().put(PROP_HELP_NAME, nameEntry.getValue());
2969            getStore().put(PROP_HELP_ORG, orgEntry.getValue());
2970            getStore().put(PROP_HELP_EMAIL, emailEntry.getValue());
2971            getStore().save();
2972
2973            List<HttpFormEntry> entriesToPost = 
2974                new ArrayList<>(entries);
2975
2976            if ((stackTrace != null) && (stackTrace.length() > 0)) {
2977                entriesToPost.remove(descriptionEntry);
2978                String newDescription =
2979                    descriptionEntry.getValue()
2980                    + "\n\n******************\nStack trace:\n" + stackTrace;
2981                entriesToPost.add(
2982                    new HttpFormEntry(
2983                        HttpFormEntry.TYPE_HIDDEN, "form_data[description]",
2984                        "Description:", newDescription, 5, 30, true));
2985            }
2986
2987            try {
2988                extra.append(idv.getPluginManager().getPluginHtml());
2989                extra.append(getResourceManager().getHtmlView());
2990
2991                entriesToPost.add(new HttpFormEntry("form_data[att_extra]",
2992                    "extra.html", extra.toString().getBytes()));
2993
2994                if (includeBundleCbx.isSelected()) {
2995                    entriesToPost.add(
2996                        new HttpFormEntry(
2997                            "form_data[att_state]", "bundle" + Constants.SUFFIX_MCV,
2998                            idv.getPersistenceManager().getBundleXml(
2999                                true).getBytes()));
3000                }
3001                entriesToPost.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 
3002                    "form_data[cc_user]", "", 
3003                    Boolean.toString(getStore().get("mcv.supportreq.cc", true))));
3004
3005                String[] results = 
3006                    HttpFormEntry.doPost(entriesToPost, SUPPORT_REQ_URL);
3007
3008                if (results[0] != null) {
3009                    GuiUtils.showHtmlDialog(
3010                        results[0], "Support Request Response - Error",
3011                        "Support Request Response - Error", null, true);
3012                    continue;
3013                }
3014                String html = results[1];
3015                if (html.toLowerCase().indexOf("your email has been sent")
3016                        >= 0) {
3017                    LogUtil.userMessage("Your support request has been sent");
3018                    break;
3019                } else if (html.toLowerCase().indexOf("required fields")
3020                           >= 0) {
3021                    LogUtil.userErrorMessage(
3022                        "<html>There was a problem submitting your request. <br>Is your email correct?</html>");
3023                } else {
3024                    GuiUtils.showHtmlDialog(
3025                        html, "Unknown Support Request Response",
3026                        "Unknown Support Request Response", null, true);
3027                    System.err.println(html.toLowerCase());
3028                }
3029            } catch (Exception exc) {
3030                LogUtil.logException("Doing support request form", exc);
3031            }
3032        }
3033        dialog.dispose();
3034    }
3035
3036    @Override protected IdvXmlUi doMakeIdvXmlUi(IdvWindow window, 
3037        List viewManagers, Element skinRoot) 
3038    {
3039        return new McIDASVXmlUi(window, viewManagers, idv, skinRoot);
3040    }
3041
3042    /**
3043     * DeInitialize the given menu before it is shown
3044     * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu)
3045     */
3046    @Override
3047    protected void handleMenuDeSelected(final String id, final JMenu menu, final IdvWindow idvWindow) {
3048        super.handleMenuDeSelected(id, menu, idvWindow);
3049    }
3050
3051    /**
3052     * Initialize the given menu before it is shown
3053     * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu)
3054     */
3055    @Override
3056    protected void handleMenuSelected(final String id, final JMenu menu, final IdvWindow idvWindow) {
3057        if (id.equals(MENU_NEWVIEWS)) {
3058            ViewManager last = getVMManager().getLastActiveViewManager();
3059            menu.removeAll();
3060            makeViewStateMenu(menu, last);
3061        } else if (id.equals("bundles")) {
3062            menu.removeAll();
3063            makeBundleMenu(menu);
3064        } else if (id.equals(MENU_NEWDISPLAY_TAB)) {
3065            menu.removeAll();
3066            doMakeNewDisplayMenu(menu, false);
3067        } else if (id.equals(MENU_NEWDISPLAY)) {
3068            menu.removeAll();
3069            doMakeNewDisplayMenu(menu, true);
3070        } else if (id.equals("menu.tools.projections.deletesaved")) {
3071            menu.removeAll();
3072            makeDeleteViewsMenu(menu);
3073        } else if (id.equals("file.default.layout")) {
3074            makeDefaultLayoutMenu(menu);
3075        } else if (id.equals("tools.formulas")) {
3076            menu.removeAll();
3077            makeFormulasMenu(menu);
3078        } else {
3079            super.handleMenuSelected(id, menu, idvWindow);
3080        }
3081    }
3082
3083    /** A cache of the operand name to value for the user choices */
3084    private Map operandCache;
3085
3086    @Override public List selectUserChoices(String msg, List userOperands) {
3087        if (operandCache == null) {
3088            operandCache =
3089                (Hashtable) getStore().getEncodedFile("operandcache.xml");
3090            if (operandCache == null) {
3091                operandCache = new Hashtable();
3092            }
3093        }
3094        List fields         = new ArrayList();
3095        List components     = new ArrayList();
3096        List persistentCbxs = new ArrayList();
3097        components.add(new JLabel("Property"));
3098        components.add(new JLabel("Value"));
3099        components.add(new JLabel("Save in Bundle"));
3100        for (int i = 0; i < userOperands.size(); i++) {
3101            DataOperand operand   = (DataOperand)userOperands.get(i);
3102            String      fieldType = operand.getProperty("type");
3103            if (fieldType == null) {
3104                fieldType = FIELDTYPE_TEXT;
3105            }
3106            DerivedDataChoice formula = operand.getDataChoice();
3107            String description = operand.getDescription();
3108            if (formula != null) {
3109                description = formula.toString();
3110            }
3111
3112            String label = operand.getLabel();
3113            Object dflt = operand.getUserDefault();
3114            Object cacheKeyNewStyle = Misc.newList(description, label, fieldType);
3115            Object cacheKey = Misc.newList(label, fieldType);
3116
3117            Object cachedOperand = null;
3118            boolean oldStyle = operandCache.containsKey(cacheKey);
3119            boolean newStyle = operandCache.containsKey(cacheKeyNewStyle);
3120
3121            // if new style, always use that and ignore old style
3122            // if no new style, proceed as before.
3123            if (newStyle) {
3124                cachedOperand = operandCache.get(cacheKeyNewStyle);
3125            } else if (oldStyle) {
3126                cachedOperand = operandCache.get(cacheKey);
3127            }
3128
3129            if (cachedOperand != null) {
3130                dflt = cachedOperand;
3131            }
3132
3133            JCheckBox cbx = new JCheckBox("", operand.isPersistent());
3134            persistentCbxs.add(cbx);
3135            JComponent field     = null;
3136            JComponent fieldComp = null;
3137            if (fieldType.equals(FIELDTYPE_TEXT)) {
3138                String rowString = operand.getProperty("rows");
3139                if (rowString == null) {
3140                    rowString = "1";
3141                }
3142                int rows = Integer.parseInt(rowString);
3143                if (rows == 1) {
3144                    field = new JTextField((dflt != null)
3145                        ? dflt.toString()
3146                        : "", 15);
3147                } else {
3148                    field     = new JTextArea((dflt != null)
3149                        ? dflt.toString()
3150                        : "", rows, 15);
3151                    fieldComp = GuiUtils.makeScrollPane(field, 200, 100);
3152                }
3153            } else if (fieldType.equals(FIELDTYPE_BOOLEAN)) {
3154                field = new JCheckBox("", ((dflt != null)
3155                    ? new Boolean(
3156                    dflt.toString()).booleanValue()
3157                    : true));
3158            } else if (fieldType.equals(FIELDTYPE_CHOICE)) {
3159                String choices = operand.getProperty("choices");
3160                if (choices == null) {
3161                    throw new IllegalArgumentException(
3162                        "No 'choices' attribute defined for operand: "
3163                            + operand);
3164                }
3165                List l = StringUtil.split(choices, ";", true, true);
3166                field = new JComboBox(new Vector(l));
3167                if ((dflt != null) && l.contains(dflt)) {
3168                    ((JComboBox) field).setSelectedItem(dflt);
3169                }
3170            } else if (fieldType.equals(FIELDTYPE_FILE)) {
3171                JTextField fileFld = new JTextField(((dflt != null)
3172                    ? dflt.toString()
3173                    : ""), 30);
3174                field = fileFld;
3175                String patterns = operand.getProperty("filepattern");
3176                List   filters  = null;
3177                if (patterns != null) {
3178                    filters = new ArrayList();
3179                    List toks = StringUtil.split(patterns, ";", true, true);
3180                    for (int tokIdx = 0; tokIdx < toks.size(); tokIdx++) {
3181                        String tok   = (String) toks.get(tokIdx);
3182                        List subToks = StringUtil.split(tok, ":", true, true);
3183                        if (subToks.size() == 2) {
3184                            filters.add(
3185                                new PatternFileFilter(
3186                                    (String)subToks.get(0),
3187                                    (String)subToks.get(1)));
3188                        } else {
3189                            filters.add(new PatternFileFilter(tok, tok));
3190                        }
3191                    }
3192                }
3193                fieldComp = GuiUtils.centerRight(GuiUtils.hfill(fileFld),
3194                    GuiUtils.makeFileBrowseButton(fileFld, filters));
3195            } else if (fieldType.equals(FIELDTYPE_LOCATION)) {
3196                List l = ((dflt != null)
3197                    ? StringUtil.split(dflt.toString(), ";", true, true)
3198                    : (List) new ArrayList());
3199                final LatLonWidget llw = new LatLonWidget();
3200                field = llw;
3201                if (l.size() == 2) {
3202                    llw.setLat(Misc.decodeLatLon(l.get(0).toString()));
3203                    llw.setLon(Misc.decodeLatLon(l.get(1).toString()));
3204                }
3205                final JButton centerPopupBtn =
3206                    GuiUtils.getImageButton("/auxdata/ui/icons/Map16.gif",
3207                        getClass());
3208                centerPopupBtn.setToolTipText("Center on current displays");
3209                centerPopupBtn.addActionListener(ae -> popupCenterMenu(centerPopupBtn, llw));
3210                JComponent centerPopup = GuiUtils.inset(centerPopupBtn,
3211                    new Insets(0, 0, 0, 4));
3212                fieldComp = GuiUtils.hbox(llw, centerPopup);
3213            } else if (fieldType.equals(FIELDTYPE_AREA)) {
3214                //TODO:
3215            } else {
3216                throw new IllegalArgumentException("Unknown type: "
3217                    + fieldType + " for operand: " + operand);
3218            }
3219
3220            fields.add(field);
3221            label = StringUtil.replace(label, "_", " ");
3222            components.add(GuiUtils.rLabel(label));
3223            components.add((fieldComp != null)
3224                ? fieldComp
3225                : field);
3226            components.add(cbx);
3227        }
3228        //        GuiUtils.tmpColFills = new int[] { GridBagConstraints.HORIZONTAL,
3229        //                                           GridBagConstraints.NONE,
3230        //                                           GridBagConstraints.NONE };
3231        GuiUtils.tmpInsets = GuiUtils.INSETS_5;
3232        Component contents = GuiUtils.topCenter(new JLabel(msg),
3233            GuiUtils.doLayout(components, 3,
3234                GuiUtils.WT_NYN, GuiUtils.WT_N));
3235        if ( !GuiUtils.showOkCancelDialog(null, "Select input", contents,
3236            null, fields)) {
3237            return null;
3238        }
3239        List values = new ArrayList();
3240        for (int i = 0; i < userOperands.size(); i++) {
3241            DataOperand operand = (DataOperand) userOperands.get(i);
3242            String description = operand.getDescription();
3243            DerivedDataChoice formula = operand.getDataChoice();
3244            String label = operand.getLabel();
3245            Object field = fields.get(i);
3246            Object value = null;
3247            Object cacheValue = null;
3248
3249            if (formula != null) {
3250                description = formula.toString();
3251            }
3252
3253            if (field instanceof JTextComponent) {
3254                value = ((JTextComponent) field).getText().trim();
3255            } else if (field instanceof JCheckBox) {
3256                value = Boolean.valueOf(((JCheckBox)field).isSelected());
3257            } else if (field instanceof JComboBox) {
3258                value = ((JComboBox) field).getSelectedItem();
3259            } else if (field instanceof LatLonWidget) {
3260                LatLonWidget llw = (LatLonWidget) field;
3261                value      = new LatLonPointImpl(llw.getLat(), llw.getLon());
3262                cacheValue = llw.getLat() + ";" + llw.getLon();
3263            } else {
3264                throw new IllegalArgumentException("Unknown field type:"
3265                    + field.getClass().getName());
3266            }
3267            if (cacheValue == null) {
3268                cacheValue = value;
3269            }
3270            JCheckBox cbx       = (JCheckBox)persistentCbxs.get(i);
3271            String    fieldType = operand.getProperty("type");
3272            if (fieldType == null) {
3273                fieldType = "text";
3274            }
3275
3276            Object cacheKey = Misc.newList(description, label, fieldType);
3277            operandCache.put(cacheKey, cacheValue);
3278            values.add(new UserOperandValue(value, cbx.isSelected()));
3279        }
3280        getStore().putEncodedFile("operandcache.xml", operandCache);
3281        return values;
3282    }
3283
3284    private boolean didTabs = false;
3285    private boolean didNewWindow = false;
3286
3287    public void makeDefaultLayoutMenu(final JMenu menu) {
3288        if (menu == null)
3289            throw new NullPointerException("Must provide a non-null default layout menu");
3290
3291        menu.removeAll();
3292        JMenuItem saveLayout = new JMenuItem("Save");
3293        McVGuiUtils.setMenuImage(saveLayout, Constants.ICON_DEFAULTLAYOUTADD_SMALL);
3294        saveLayout.setToolTipText("Save as default layout");
3295        saveLayout.addActionListener(e -> ((McIDASV)idv).doSaveAsDefaultLayout());
3296
3297        JMenuItem removeLayout = new JMenuItem("Remove");
3298        McVGuiUtils.setMenuImage(removeLayout, Constants.ICON_DEFAULTLAYOUTDELETE_SMALL);
3299        removeLayout.setToolTipText("Remove saved default layout");
3300        removeLayout.addActionListener(e -> idv.doClearDefaults());
3301
3302        removeLayout.setEnabled(((McIDASV)idv).hasDefaultLayout());
3303
3304        menu.add(saveLayout);
3305        menu.add(removeLayout);
3306    }
3307
3308    /**
3309     * Bundles any compatible {@link ViewManager} states into
3310     * {@link JMenuItem JMenuItem} and adds said menu items to {@code menu}.
3311     * Incompatible states are ignored.
3312     * 
3313     * <p>Each {@code JMenuItem} (except those under the {@literal "Delete"}
3314     * menu--apologies) associates a {@literal "view state"} and an
3315     * {@link ObjectListener}. The {@code ObjectListener} uses this associated
3316     * view state to attempt reinitialization of {@code vm}.
3317     * 
3318     * <p>Override reasoning:
3319     * <ul>
3320     *   <li>
3321     *     terminology ({@literal "views"} rather than {@literal "viewpoints"}).
3322     *   </li>
3323     *   <li>
3324     *     use of {@link #filterVMMStatesWithVM(ViewManager, Collection)} to
3325     *     properly detect the {@literal "no saved views"} case.
3326     *   </li>
3327     * </ul>
3328     * 
3329     * @param menu Menu to populate. Should not be {@code null}.
3330     * @param vm {@code ViewManager} that might get reinitialized.
3331     * Should not be {@code null}.
3332     * 
3333     * @see ViewManager#initWith(ViewManager, boolean)
3334     * @see ViewManager#initWith(ViewState)
3335     */
3336    @Override public void makeViewStateMenu(final JMenu menu, final ViewManager vm) {
3337        List<TwoFacedObject> vmStates =
3338            filterVMMStatesWithVM(vm, getVMManager().getVMState());
3339        if (vmStates.isEmpty()) {
3340            JMenuItem item = new JMenuItem(Msg.msg("No Saved Views"));
3341            item.setEnabled(false);
3342            menu.add(item);
3343        } else {
3344            JMenu deleteMenu = new JMenu("Delete");
3345            makeDeleteViewsMenu(deleteMenu);
3346            menu.add(deleteMenu);
3347        }
3348
3349        // McIDAS Inquiry #2803-3141 
3350        ArrayList<TwoFacedObject> notInDirs = new ArrayList<>();
3351        HashMap<String, JMenu> dirs = new HashMap<>();
3352
3353        for (int i = 0; i < vmStates.size(); i++) {
3354            TwoFacedObject tfo = vmStates.get(i);
3355
3356            if (tfo.getLabel().toString().contains(">")) {
3357                String dirStr = tfo.getLabel().toString();
3358                String[] dirAsArray = dirStr.split(">");
3359
3360                if (dirAsArray.length > 2) {
3361                    String tgt = "";
3362                    for (int split = 1; split < dirAsArray.length; split++) {
3363                        tgt += dirAsArray[split] + ">";
3364                    }
3365                    dirAsArray[1] = tgt;
3366                }
3367
3368                JMenu tmp;
3369                if (dirs.containsKey(dirAsArray[0])) {
3370                    tmp = dirs.get(dirAsArray[0]);
3371                } else {
3372                    tmp = new JMenu(dirAsArray[0]);
3373                }
3374                JMenuItem mi = new JMenuItem(dirAsArray[1]);
3375                tmp.add(mi);
3376                dirs.put(dirAsArray[0], tmp);
3377
3378                mi.addActionListener(new ObjectListener(tfo.getId()) {
3379                    public void actionPerformed(final ActionEvent e) {
3380                        if (vm == null) {
3381                            return;
3382                        }
3383                        if (theObject instanceof ViewManager) {
3384                            vm.initWith((ViewManager)theObject, true);
3385                        } else if (theObject instanceof ViewState) {
3386                            try {
3387                                vm.initWith((ViewState)theObject);
3388                            } catch (Throwable ex) {
3389                                logException("Initializing view with ViewState", ex);
3390                            }
3391                        } else {
3392                            LogUtil.consoleMessage("UIManager.makeViewStateMenu: Object of unknown type: "+theObject.getClass().getName());
3393                        }
3394                    }
3395                });
3396
3397            } else {
3398                notInDirs.add(tfo);
3399            }
3400        }
3401
3402        ArrayList<JMenu> folders = new ArrayList<>();
3403
3404        for (Map.Entry<String, JMenu> entry : dirs.entrySet()) {
3405            JMenu jmu = entry.getValue();
3406            ArrayList<JMenuItem> itms = new ArrayList<>();
3407
3408            for (int i = 0; i < jmu.getItemCount(); i++) {
3409                itms.add(jmu.getItem(i));
3410            }
3411
3412            Collections.sort(itms, new Comparator<JMenuItem>() {
3413                @Override
3414                public int compare(JMenuItem o1, JMenuItem o2) {
3415                    return (o1.getText().toLowerCase().compareTo(o2.getText().toLowerCase()));
3416                }
3417            });
3418
3419            JMenu out = new JMenu(jmu.getText());
3420
3421            for (int i = 0; i < itms.size(); i++) {
3422                out.add(itms.get(i));
3423            }
3424
3425            folders.add(out);
3426        }
3427
3428        Collections.sort(folders, new Comparator<JMenu>() {
3429            @Override
3430            public int compare(JMenu o1, JMenu o2) {
3431                return (o1.getText().toLowerCase().compareTo(o2.getText().toLowerCase()));
3432            }
3433        });
3434
3435        for (int i = 0; i < folders.size(); i++) {
3436            menu.add(folders.get(i));
3437        }
3438
3439        if (folders.size() > 0) {
3440            menu.addSeparator();
3441        }
3442
3443        Collections.sort(notInDirs, new  Comparator<TwoFacedObject>() {
3444            @Override
3445            public int compare(TwoFacedObject a, TwoFacedObject b) {
3446                return (a.getLabel().toString().toLowerCase().compareTo(b.getLabel().toString().toLowerCase()));
3447            }
3448        });
3449
3450        for (TwoFacedObject tfo : notInDirs) {
3451            JMenuItem mi = new JMenuItem(tfo.getLabel().toString());
3452            menu.add(mi);
3453            mi.addActionListener(new ObjectListener(tfo.getId()) {
3454                public void actionPerformed(final ActionEvent e) {
3455                    if (vm == null) {
3456                        return;
3457                    }
3458
3459                    if (theObject instanceof ViewManager) {
3460                        vm.initWith((ViewManager)theObject, true);
3461                    } else if (theObject instanceof ViewState) {
3462                        try {
3463                            vm.initWith((ViewState)theObject);
3464                        } catch (Throwable ex) {
3465                            logException("Initializing view with ViewState", ex);
3466                        }
3467                    } else {
3468                        LogUtil.consoleMessage("UIManager.makeViewStateMenu: Object of unknown type: "+theObject.getClass().getName());
3469                    }
3470                }
3471            });
3472        }
3473
3474        // the "3" ensures that the "save viewpoint" menu item, the separator,
3475        // and the "delete" menu item are fixed at the top.
3476        new MenuScroller(menu, menu, 125, 3);
3477    }
3478
3479    /**
3480     * Overridden by McIDAS-V to add menu scrolling functionality to the
3481     * {@literal "delete"} submenu.
3482     *
3483     * @param menu {@literal "Delete"} submenu.
3484     */
3485    @Override public void makeDeleteViewsMenu(JMenu menu) {
3486        super.makeDeleteViewsMenu(menu);
3487        new MenuScroller(menu, menu, 125);
3488    }
3489
3490    /**
3491     * Returns a list of {@link TwoFacedObject}s that are known to be 
3492     * compatible with {@code vm}.
3493     * 
3494     * <p>This method is currently capable of dealing with
3495     * {@link TwoFacedObject TwoFacedObjects} and
3496     * {@link ViewState ViewStates} within {@code states}. Any other types are
3497     * ignored.
3498     * 
3499     * @param vm {@link ViewManager} to use for compatibility tests.
3500     * {@code null} is allowed.
3501     * @param states Collection of objects to test against {@code vm}.
3502     * {@code null} is allowed.
3503     * 
3504     * @return Either a {@link List} of compatible {@literal "view states"}
3505     * or an empty {@code List}.
3506     * 
3507     * @see ViewManager#isCompatibleWith(ViewManager)
3508     * @see ViewManager#isCompatibleWith(ViewState)
3509     * @see #makeViewStateMenu(JMenu, ViewManager)
3510     */
3511    public static List<TwoFacedObject> filterVMMStatesWithVM(final ViewManager vm, final Collection<?> states) {
3512        if ((vm == null) || (states == null) || states.isEmpty()) {
3513            return Collections.emptyList();
3514        }
3515
3516        List<TwoFacedObject> validStates = new ArrayList<>(states.size());
3517        for (Object obj : states) {
3518            TwoFacedObject tfo = null;
3519            if (obj instanceof TwoFacedObject) {
3520                tfo = (TwoFacedObject)obj;
3521                if (vm.isCompatibleWith((ViewManager)tfo.getId())) {
3522                    continue;
3523                }
3524            } else if (obj instanceof ViewState) {
3525                if (!vm.isCompatibleWith((ViewState)obj)) {
3526                    continue;
3527                }
3528                tfo = new TwoFacedObject(((ViewState)obj).getName(), obj);
3529            } else {
3530                LogUtil.consoleMessage("UIManager.filterVMMStatesWithVM: Object of unknown type: "+obj.getClass().getName());
3531                continue;
3532            }
3533            validStates.add(tfo);
3534        }
3535        return validStates;
3536    }
3537
3538    /**
3539     * Overridden to build a custom Display menu.
3540     * @see ucar.unidata.idv.ui.IdvUIManager#initializeDisplayMenu(JMenu)
3541     */
3542    @Override protected void initializeDisplayMenu(JMenu displayMenu) {
3543        JMenu m;
3544        JMenuItem mi;
3545
3546        // Get the list of possible standalone control descriptors
3547        Hashtable controlsHash = new Hashtable();
3548        List controlDescriptors = getStandAloneControlDescriptors();
3549        for (int i = 0; i < controlDescriptors.size(); i++) {
3550            ControlDescriptor cd = (ControlDescriptor)controlDescriptors.get(i);
3551            String cdLabel = cd.getLabel();
3552            if (cdLabel.equals("Range Rings")) {
3553                controlsHash.put(cdLabel, cd);
3554            } else if (cdLabel.equals("Range and Bearing")) {
3555                controlsHash.put(cdLabel, cd);
3556            } else if (cdLabel.equals("Location Indicator")) {
3557                controlsHash.put(cdLabel, cd);
3558            } else if (cdLabel.equals("Drawing Control")) {
3559                controlsHash.put(cdLabel, cd);
3560            } else if (cdLabel.equals("Transect Drawing Control")) {
3561                controlsHash.put(cdLabel, cd);
3562            }
3563        }
3564
3565        // Build the menu
3566        ControlDescriptor cd;
3567
3568        mi = new JMenuItem("Create Layer from Data Source...");
3569        mi.addActionListener(ae -> showDashboard("Data Sources"));
3570        mi.setToolTipText("Open Data Sources tab of Data Explorer");
3571        displayMenu.add(mi);
3572
3573        mi = new JMenuItem("Layer Controls...");
3574        mi.addActionListener(ae -> showDashboard("Layer Controls"));
3575        mi.setToolTipText("Open Layer Controls tab of Data Explorer");
3576        displayMenu.add(mi);
3577
3578        displayMenu.addSeparator();
3579
3580        cd = (ControlDescriptor)controlsHash.get("Range Rings");
3581        mi = makeControlDescriptorItem(cd);
3582        mi.setText("Add Range Rings");
3583        mi.setToolTipText("Add Range Rings to the Main Display");
3584        displayMenu.add(mi);
3585
3586        cd = (ControlDescriptor)controlsHash.get("Range and Bearing");
3587        mi = makeControlDescriptorItem(cd);
3588        McVGuiUtils.setMenuImage(mi, Constants.ICON_RANGEANDBEARING_SMALL);
3589        mi.setText("Add Range and Bearing");
3590        mi.setToolTipText("Add Range and Bearing to the Main Display");
3591        displayMenu.add(mi);
3592
3593        displayMenu.addSeparator();
3594
3595        cd = (ControlDescriptor)controlsHash.get("Transect Drawing Control");
3596        mi = makeControlDescriptorItem(cd);
3597        mi.setText("Draw Transect...");
3598        mi.setToolTipText("Add a transect line to the Main Display");
3599        displayMenu.add(mi);
3600
3601        cd = (ControlDescriptor)controlsHash.get("Drawing Control");
3602        mi = makeControlDescriptorItem(cd);
3603        mi.setText("Draw Freely...");
3604        mi.setToolTipText("Add a Drawing Control layer to the Main Display");
3605        displayMenu.add(mi);
3606
3607        displayMenu.addSeparator();
3608
3609        cd = (ControlDescriptor)controlsHash.get("Location Indicator");
3610        mi = makeControlDescriptorItem(cd);
3611        McVGuiUtils.setMenuImage(mi, Constants.ICON_LOCATION_SMALL);
3612        mi.setText("Add Location Indicator");
3613        mi.setToolTipText("Add a Location Indicator to the Main Display");
3614        displayMenu.add(mi);
3615
3616        ControlDescriptor locationDescriptor = idv.getControlDescriptor("locationcontrol");
3617        if (locationDescriptor != null) {
3618            List stations = idv.getLocationList();
3619            ObjectListener listener = new ObjectListener(locationDescriptor) {
3620                public void actionPerformed(ActionEvent ae, Object obj) {
3621                    addStationDisplay((NamedStationTable) obj, (ControlDescriptor) theObject);
3622                }
3623            };
3624            List menuItems = NamedStationTable.makeMenuItems(stations, listener);
3625            displayMenu.add(MenuUtil.makeMenu("Plot Location Labels", menuItems));
3626        }
3627
3628        displayMenu.addSeparator();
3629
3630        mi = new JMenuItem("Add Background Image");
3631        McVGuiUtils.setMenuImage(mi, Constants.ICON_BACKGROUND_SMALL);
3632        mi.addActionListener(ae -> getIdv().doMakeBackgroundImage());
3633        mi.setToolTipText("Add a WMS background to the Main Display");
3634        displayMenu.add(mi);
3635
3636        mi = new JMenuItem("Reset Map Layer to Defaults");
3637        mi.setToolTipText("Return the map layer to default settings");
3638        mi.addActionListener(ae -> {
3639            // TODO: Call IdvUIManager.addDefaultMap()... should be made private
3640//                addDefaultMap();
3641            ControlDescriptor mapDescriptor = idv.getControlDescriptor("mapdisplay");
3642            if (mapDescriptor == null) {
3643                return;
3644            }
3645            String attrs = "initializeAsDefault=true;displayName=Default Background Maps;";
3646            idv.doMakeControl(new ArrayList(), mapDescriptor, attrs, null);
3647        });
3648        displayMenu.add(mi);
3649        Msg.translateTree(displayMenu);
3650    }
3651
3652    /**
3653     * Get the window title from the skin
3654     *
3655     * @param index  the skin index
3656     *
3657     * @return  the title
3658     */
3659    private String getWindowTitleFromSkin(final int index) {
3660        if (!skinToTitle.containsKey(index)) {
3661            IdvResourceManager mngr = getResourceManager();
3662            XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN);
3663            List<String> names = StringUtil.split(skins.getShortName(index), ">", true, true);
3664            String title = getStateManager().getTitle();
3665            if (!names.isEmpty()) {
3666                title = title + " - " + StringUtil.join(" - ", names);
3667            }
3668            skinToTitle.put(index, title);
3669        }
3670        return skinToTitle.get(index);
3671    }
3672
3673    @SuppressWarnings("unchecked")
3674    @Override public Hashtable getMenuIds() {
3675        return menuIds;
3676    }
3677
3678    @SuppressWarnings("unchecked")
3679    @Override public JMenuBar doMakeMenuBar(final IdvWindow idvWindow) {
3680        Hashtable<String, JMenuItem> menuMap = new Hashtable<>();
3681        JMenuBar menuBar = new JMenuBar();
3682        final IdvResourceManager mngr = getResourceManager();
3683        XmlResourceCollection xrc = mngr.getXmlResources(mngr.RSC_MENUBAR);
3684        Hashtable<String, ImageIcon> actionIcons = new Hashtable<>();
3685
3686        for (int i = 0; i < xrc.size(); i++) {
3687            GuiUtils.processXmlMenuBar(xrc.getRoot(i), menuBar, getIdv(), menuMap, actionIcons);
3688        }
3689
3690        menuIds = new Hashtable<>(menuMap);
3691
3692        // Ensure that the "help" menu is the last menu.
3693        JMenuItem helpMenu = menuMap.get(MENU_HELP);
3694        if (helpMenu != null) {
3695            menuBar.remove(helpMenu);
3696            menuBar.add(helpMenu);
3697        }
3698
3699        //TODO: Perhaps we will put the different skins in the menu?
3700        JMenu newDisplayMenu = (JMenu)menuMap.get(MENU_NEWDISPLAY);
3701        if (newDisplayMenu != null) {
3702            MenuUtil.makeMenu(newDisplayMenu, makeSkinMenuItems(makeMenuBarActionListener(), true, false));
3703        }
3704
3705//        final JMenu publishMenu = menuMap.get(MENU_PUBLISH);
3706//        if (publishMenu != null) {
3707//            if (!getPublishManager().isPublishingEnabled())
3708//                publishMenu.getParent().remove(publishMenu);
3709//            else
3710//                getPublishManager().initMenu(publishMenu);
3711//        }
3712
3713        for (Entry<String, JMenuItem> e : menuMap.entrySet()) {
3714            if (!(e.getValue() instanceof JMenu)) {
3715                continue;
3716            }
3717            String menuId = e.getKey();
3718            JMenu menu = (JMenu)e.getValue();
3719            menu.addMenuListener(makeMenuBarListener(menuId, menu, idvWindow));
3720        }
3721        return menuBar;
3722    }
3723
3724    private final ActionListener makeMenuBarActionListener() {
3725        final IdvResourceManager mngr = getResourceManager();
3726        return ae -> {
3727            XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN);
3728            int skinIndex = ((Integer)ae.getSource()).intValue();
3729            createNewWindow(null, true, getWindowTitleFromSkin(skinIndex),
3730                skins.get(skinIndex).toString(),
3731                skins.getRoot(skinIndex, false), true, null);
3732        };
3733    }
3734
3735    private final MenuListener makeMenuBarListener(final String id, final JMenu menu, final IdvWindow idvWindow) {
3736        return new MenuListener() {
3737            public void menuCanceled(final MenuEvent e) { }
3738            public void menuDeselected(final MenuEvent e) { handleMenuDeSelected(id, menu, idvWindow); }
3739            public void menuSelected(final MenuEvent e) { handleMenuSelected(id, menu, idvWindow); }
3740        };
3741    }
3742
3743    /**
3744     * Handle mouse clicks that occur within the toolbar.
3745     */
3746    private class PopupListener extends MouseAdapter {
3747
3748        private JPopupMenu popup;
3749
3750        public PopupListener(JPopupMenu p) {
3751            popup = p;
3752        }
3753
3754        // handle right clicks on os x and linux
3755        public void mousePressed(MouseEvent e) {
3756            if (e.isPopupTrigger()) {
3757                popup.show(e.getComponent(), e.getX(), e.getY());
3758            }
3759        }
3760
3761        // Windows doesn't seem to trigger mousePressed() for right clicks, but
3762        // never fear; mouseReleased() does the job.
3763        public void mouseReleased(MouseEvent e) {
3764            if (e.isPopupTrigger()) {
3765                popup.show(e.getComponent(), e.getX(), e.getY());
3766            }
3767        }
3768    }
3769
3770    /**
3771     * Handle (polymorphically) the {@link DataControlDialog}.
3772     *
3773     * This dialog is used to either select a display control to create
3774     * or is used to set the timers used for a {@link ucar.unidata.data.DataSource}.
3775     *
3776     * @param dcd The dialog
3777     */
3778    public void processDialog(DataControlDialog dcd) {
3779        int estimatedMB = getEstimatedMegabytes(dcd);
3780        if (estimatedMB > 0) {
3781            double totalMem = Runtime.getRuntime().maxMemory();
3782            double highMem = Runtime.getRuntime().totalMemory();
3783            double freeMem = Runtime.getRuntime().freeMemory();
3784            double usedMem = (highMem - freeMem);
3785            int availableMB = Math.round( ((float)totalMem - (float)usedMem) / 1024f / 1024f);
3786            int percentOfAvailable = Math.round((float)estimatedMB / (float)availableMB * 100f);
3787            if (percentOfAvailable > 95) {
3788                String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>";
3789                message += "which exceeds 95% of total amount available (" + availableMB +"MB).<br>";
3790                message += "Data load cancelled.</html>";
3791                JComponent msgLabel = new JLabel(message);
3792                GuiUtils.showDialog("Data Size", msgLabel);
3793                return;
3794            } else if (percentOfAvailable >= 75) {
3795                String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>";
3796                message += percentOfAvailable + "% of the total amount available (" + availableMB + "MB).<br>";
3797                message += "Continue loading data?</html>";
3798                JComponent msgLabel = new JLabel(message);
3799                if (!GuiUtils.askOkCancel("Data Size", msgLabel)) {
3800                    return;
3801                }
3802            }
3803        }
3804        super.processDialog(dcd);
3805    }
3806
3807    /**
3808     * Estimate the number of megabytes that will be used by this data selection
3809     * 
3810     * @param dcd Data control dialog containing the data selection whose size
3811     *            we'd like to estimate. Cannot be {@code null}.
3812     *            
3813     * @return Rough estimate of the size (in megabytes) of the 
3814     * {@code DataSelection} within {@code dcd}.
3815     */
3816    protected int getEstimatedMegabytes(DataControlDialog dcd) {
3817        int estimatedMB = 0;
3818        DataChoice dataChoice = dcd.getDataChoice();
3819        if (dataChoice != null) {
3820            Object[] selectedControls = dcd.getSelectedControls();
3821            for (int i = 0; i < selectedControls.length; i++) {
3822                ControlDescriptor cd = (ControlDescriptor) selectedControls[i];
3823
3824                //Check if the data selection is ok
3825                if(!dcd.getDataSelectionWidget().okToCreateTheDisplay(cd.doesLevels())) {
3826                    continue;
3827                }
3828
3829                DataSelection dataSelection = dcd.getDataSelectionWidget().createDataSelection(cd.doesLevels());
3830                                
3831                // Get the size in pixels of the requested image
3832                Object gotSize = dataSelection.getProperty("SIZE");
3833                if (gotSize == null) {
3834                        continue;
3835                }
3836                List<String> dims = StringUtil.split(gotSize, " ", false, false);
3837                int myLines = -1;
3838                int myElements = -1;
3839                if (dims.size() == 2) {
3840                    try {
3841                        myLines = Integer.parseInt(dims.get(0));
3842                        myElements = Integer.parseInt(dims.get(1));
3843                    }
3844                    catch (Exception e) { }
3845                }
3846
3847                // Get the count of times requested
3848                int timeCount = 1;
3849                DataSelectionWidget dsw = dcd.getDataSelectionWidget();
3850                List times = dsw.getSelectedDateTimes();
3851                List timesAll = dsw.getAllDateTimes();
3852                if ((times != null) && !times.isEmpty()) {
3853                    timeCount = times.size();
3854                } else if ((timesAll != null) && !timesAll.isEmpty()) {
3855                    timeCount = timesAll.size();
3856                }
3857                
3858                // Total number of pixels
3859                // Assumed lines x elements x times x 4bytes
3860                // Empirically seems to be taking *twice* that (64bit fields??)
3861                float totalPixels = (float)myLines * (float)myElements * (float)timeCount;
3862                float totalBytes = totalPixels * 4 * 2;
3863                estimatedMB += Math.round(totalBytes / 1024f / 1024f);
3864
3865                int additionalMB = 0;
3866                // Empirical tests show that textures are not affecting
3867                // required memory... comment out for now
3868                /*
3869                int textureDimensions = 2048;
3870                int mbPerTexture = Math.round((float)textureDimensions * (float)textureDimensions * 4 / 1024f / 1024f);
3871                int textureCount = (int)Math.ceil((float)myLines / 2048f) * (int)Math.ceil((float)myElements / 2048f);
3872                int additionalMB = textureCount * mbPerTexture * timeCount;
3873                */
3874                estimatedMB += additionalMB;
3875            }
3876        }
3877        return estimatedMB;
3878    }
3879
3880    /**
3881     * Represents a SavedBundle as a tree.
3882     */
3883    private class BundleTreeNode {
3884
3885        private String name;
3886
3887        private SavedBundle bundle;
3888
3889        private List<BundleTreeNode> kids;
3890
3891        /**
3892         * This constructor is used to build a node that is considered a
3893         * {@literal "parent"}.
3894         *
3895         * These nodes only have child nodes, no {@code SavedBundles}. This
3896         * was done so that distinguishing between bundles and bundle
3897         * subcategories would be easy.
3898         * 
3899         * @param name The name of this node. For a parent node with
3900         * {@literal "Toolbar>cat"} as the path, the name parameter would
3901         * contain only {@literal "cat"}.
3902         */
3903        public BundleTreeNode(String name) {
3904            this(name, null);
3905        }
3906
3907        /**
3908         * Nodes constructed using this constructor can only ever be child
3909         * nodes.
3910         * 
3911         * @param name The name of the SavedBundle.
3912         * @param bundle A reference to the SavedBundle.
3913         */
3914        public BundleTreeNode(String name, SavedBundle bundle) {
3915            this.name = name;
3916            this.bundle = bundle;
3917            kids = new LinkedList<>();
3918        }
3919
3920        /**
3921         * @param child The node to be added to the current node.
3922         */
3923        public void addChild(BundleTreeNode child) {
3924            kids.add(child);
3925        }
3926
3927        /**
3928         * @return Returns all child nodes of this node.
3929         */
3930        public List<BundleTreeNode> getChildren() {
3931            return kids;
3932        }
3933
3934        /**
3935         * @return Return the SavedBundle associated with this node (if any).
3936         */
3937        public SavedBundle getBundle() {
3938            return bundle;
3939        }
3940
3941        /**
3942         * @return The name of this node.
3943         */
3944        public String getName() {
3945            return name;
3946        }
3947    }
3948
3949    /**
3950     * A type of {@code HttpFormEntry} that supports line wrapping for
3951     * text area entries.
3952     * 
3953     * @see HttpFormEntry
3954     */
3955    private static class FormEntry extends HttpFormEntry {
3956        /** Initial contents of this entry. */
3957        private String value = "";
3958
3959        /** Whether or not the JTextArea should wrap lines. */
3960        private boolean wrap = true;
3961
3962        /** Entry type. Used to remain compatible with the IDV. */
3963        private int type = HttpFormEntry.TYPE_AREA;
3964
3965        /** Number of rows in the JTextArea. */
3966        private int rows = 5;
3967
3968        /** Number of columns in the JTextArea. */
3969        private int cols = 30;
3970
3971        /** GUI representation of this entry. */
3972        private JTextArea component = new JTextArea(value, rows, cols);
3973
3974        /**
3975         * Required to keep Java happy.
3976         */
3977        public FormEntry() {
3978            super(HttpFormEntry.TYPE_AREA, "form_data[description]", 
3979                "Description:");
3980        }
3981
3982        /**
3983         * Using this constructor allows McIDAS-V to control whether or not a
3984         * HttpFormEntry performs line wrapping for JTextArea components.
3985         * 
3986         * @param wrap Whether or not line wrapping should be enabled.
3987         * @param type Type of this entry
3988         * @param name Name
3989         * @param label Label
3990         * @param value Initial value
3991         * @param rows Number of rows.
3992         * @param cols Number of columns.
3993         * @param required Whether or not the entry will be required.
3994         */
3995        public FormEntry(boolean wrap, int type, String name, String label, String value, int rows, int cols, boolean required) {
3996            super(type, name, label, value, rows, cols, required);
3997            this.type = type;
3998            this.rows = rows;
3999            this.cols = cols;
4000            this.wrap = wrap;
4001        }
4002
4003        /**
4004         * Overrides the IDV method so that the McIDAS-V support request form
4005         * will wrap lines in the "Description" field.
4006         * 
4007         * @param guiComps List to which this instance should be added.
4008         */
4009        @SuppressWarnings("unchecked")
4010        @Override public void addToGui(List guiComps) {
4011            if (type == HttpFormEntry.TYPE_AREA) {
4012                Dimension minSize = new Dimension(500, 200);
4013                guiComps.add(LayoutUtil.top(GuiUtils.rLabel(getLabel())));
4014                component.setLineWrap(wrap);
4015                component.setWrapStyleWord(wrap);
4016                JScrollPane sp = new JScrollPane(component);
4017                sp.setPreferredSize(minSize);
4018                sp.setMinimumSize(minSize);
4019                guiComps.add(sp);
4020            } else {
4021                super.addToGui(guiComps);
4022            }
4023        }
4024
4025        /**
4026         * Since the IDV doesn't provide a getComponent for 
4027         * {@code addToGui}, we must make our {@code component} field
4028         * local to this class. 
4029         * Hijacks any value requests so that the local {@code component}
4030         * field is queried, not the IDV's.
4031         * 
4032         * @return Contents of form.
4033         */
4034        @Override public String getValue() {
4035            if (type != HttpFormEntry.TYPE_AREA) {
4036                return super.getValue();
4037            }
4038            return component.getText();
4039        }
4040
4041        /**
4042         * Hijacks any requests to set the {@code component} field's text.
4043         */
4044        @Override public void setValue(final String newValue) {
4045            if (type == HttpFormEntry.TYPE_AREA) {
4046                component.setText(newValue);
4047            } else {
4048                super.setValue(newValue);
4049            }
4050        }
4051    }
4052
4053    /**
4054     * A {@code ToolbarStyle} is a representation of the way icons associated
4055     * with current toolbar actions should be displayed. This notion is so far
4056     * limited to the sizing of icons, but that may change.
4057     */
4058    public enum ToolbarStyle {
4059        /**
4060         * Represents the current toolbar actions as large icons. Currently,
4061         * {@literal "large"} is defined as {@code 32 x 32} pixels.
4062         */
4063        LARGE("Large Icons", "action.icons.large", 32),
4064
4065        /**
4066         * Represents the current toolbar actions as medium icons. Currently,
4067         * {@literal "medium"} is defined as {@code 22 x 22} pixels.
4068         */
4069        MEDIUM("Medium Icons", "action.icons.medium", 22),
4070
4071        /** 
4072         * Represents the current toolbar actions as small icons. Currently,
4073         * {@literal "small"} is defined as {@code 16 x 16} pixels. 
4074         */
4075        SMALL("Small Icons", "action.icons.small", 16);
4076
4077        /** Label to use in the toolbar customization popup menu. */
4078        private final String label;
4079
4080        /** Signals that the user selected a specific icon size. */
4081        private final String action;
4082
4083        /** Icon dimensions. Each icon should be {@code size * size}. */
4084        private final int size;
4085
4086        /**
4087         * {@link #size} in {@link String} form, merely for use with the IDV's
4088         * preference functionality.
4089         */
4090        private final String sizeAsString;
4091
4092        /**
4093         * Initializes a toolbar style.
4094         * 
4095         * @param label Label used in the toolbar popup menu.
4096         * @param action Command that signals the user selected this toolbar 
4097         * style.
4098         * @param size Dimensions of the icons.
4099         * 
4100         * @throws NullPointerException if {@code label} or {@code action} are
4101         * null.
4102         * 
4103         * @throws IllegalArgumentException if {@code size} is not positive.
4104         */
4105        ToolbarStyle(final String label, final String action, final int size) {
4106            requireNonNull(label, "Label cannot be null.");
4107            requireNonNull(action, "Action cannot be null.");
4108
4109            if (size <= 0) {
4110                throw new IllegalArgumentException("Size must be a positive integer");
4111            }
4112
4113            this.label = label;
4114            this.action = action;
4115            this.size = size;
4116            this.sizeAsString = Integer.toString(size);
4117        }
4118
4119        /**
4120         * Returns the label to use as a brief description of this style.
4121         * 
4122         * @return Description of style (suitable for a label).
4123         */
4124        public String getLabel() {
4125            return label;
4126        }
4127
4128        /**
4129         * Returns the action command associated with this style.
4130         * 
4131         * @return This style's {@literal "action command"}.
4132         */
4133        public String getAction() {
4134            return action;
4135        }
4136
4137        /**
4138         * Returns the dimensions of icons used in this style.
4139         * 
4140         * @return Dimensions of this style's icons.
4141         */
4142        public int getSize() {
4143            return size;
4144        }
4145
4146        /**
4147         * Returns {@link #size} as a {@link String} to make cooperating with
4148         * the IDV preferences code easier.
4149         * 
4150         * @return String representation of this style's icon dimensions.
4151         */
4152        public String getSizeAsString() {
4153            return sizeAsString;
4154        }
4155
4156        /**
4157         * Returns a brief description of this {@code ToolbarStyle}.
4158         *
4159         * <p>A typical example:
4160         * {@code [ToolbarStyle@1337: label="Large Icons", size=32]}
4161         * 
4162         * <p>Note that the format and details provided are subject to change.
4163         *
4164         * @return String representation of this {@code ToolbarStyle} instance.
4165         */
4166        public String toString() {
4167            return String.format("[ToolbarStyle@%x: label=%s, size=%d]", 
4168                hashCode(), label, size);
4169        }
4170
4171        /**
4172         * Convenience method for build the toolbar customization popup menu.
4173         * 
4174         * @param manager {@link UIManager} that will be listening for action
4175         * commands.
4176         * 
4177         * @return Menu item that has {@code manager} listening for 
4178         * {@link #action}.
4179         */
4180        protected JMenuItem buildMenuItem(final UIManager manager) {
4181            JMenuItem item = new JRadioButtonMenuItem(label);
4182            item.setActionCommand(action);
4183            item.addActionListener(manager);
4184            return item;
4185        }
4186    }
4187
4188    /**
4189     * Represents what McIDAS-V {@literal "knows"} about IDV actions.
4190     */
4191    protected enum ActionAttribute {
4192
4193        /**
4194         * Unique identifier for an IDV action. Required attribute.
4195         * 
4196         * @see IdvUIManager#ATTR_ID
4197         */
4198        ID(ATTR_ID), 
4199
4200        /**
4201         * Path to an icon for this action. Currently required. Note that 
4202         * McIDAS-V differs from the IDV in that actions must support different
4203         * icon sizes. This is implemented in McIDAS-V by simply having the value
4204         * of this path be a valid {@literal "format string"}, 
4205         * such as {@code image="/edu/wisc/ssec/mcidasv/resources/icons/toolbar/background-image%d.png"}
4206         * 
4207         * <p>The upshot is that this value <b>will not be a valid path in 
4208         * McIDAS-V</b>. Use either {@link IdvAction#getMenuIcon()} or 
4209         * {@link IdvAction#getIconForStyle}.
4210         * 
4211         * @see IdvUIManager#ATTR_IMAGE
4212         * @see IdvAction#getRawIconPath()
4213         * @see IdvAction#getMenuIcon()
4214         * @see IdvAction#getIconForStyle
4215         */
4216        ICON(ATTR_IMAGE), 
4217
4218        /**
4219         * Brief description of a IDV action. Required attribute.
4220         * @see IdvUIManager#ATTR_DESCRIPTION
4221         */
4222        DESCRIPTION(ATTR_DESCRIPTION), 
4223
4224        /**
4225         * Allows actions to be clustered into arbitrary groups. Currently 
4226         * optional; defaults to {@literal "General"}.
4227         * @see IdvUIManager#ATTR_GROUP
4228         */
4229        GROUP(ATTR_GROUP, "General"), 
4230
4231        /**
4232         * Actual method call used to invoke a given IDV action. Required 
4233         * attribute.
4234         * @see IdvUIManager#ATTR_ACTION
4235         */
4236        ACTION(ATTR_ACTION);
4237
4238        /**
4239         * A blank {@link String} if this is a required attribute, or a 
4240         * {@code String} value to use in case this attribute has not been 
4241         * specified by a given IDV action.
4242         */
4243        private final String defaultValue;
4244
4245        /**
4246         * String representation of this attribute as used by the IDV.
4247         * @see #asIdvString()
4248         */
4249        private final String idvString;
4250
4251        /** Whether or not this attribute is required. */
4252        private final boolean required;
4253
4254        /**
4255         * Creates a constant that represents a required IDV action attribute.
4256         * 
4257         * @param idvString Corresponding IDV attribute {@link String}. Cannot be {@code null}.
4258         * 
4259         * @throws NullPointerException if {@code idvString} is {@code null}.
4260         */
4261        ActionAttribute(final String idvString) {
4262            requireNonNull(idvString, "Cannot be associated with a null IDV action attribute String");
4263
4264            this.idvString = idvString; 
4265            this.defaultValue = ""; 
4266            this.required = true; 
4267        }
4268
4269        /**
4270         * Creates a constant that represents an optional IDV action attribute.
4271         * 
4272         * @param idvString Corresponding IDV attribute {@link String}. 
4273         * Cannot be {@code null}.
4274         * @param defValue Default value for actions that do not have this 
4275         * attribute. Cannot be {@code null} or an empty {@code String}.
4276         * 
4277         * @throws NullPointerException if either {@code idvString} or 
4278         * {@code defValue} is {@code null}.
4279         * @throws IllegalArgumentException if {@code defValue} is an empty 
4280         * {@code String}.
4281         * 
4282         */
4283        ActionAttribute(final String idvString, final String defValue) {
4284            requireNonNull(idvString, "Cannot be associated with a null IDV action attribute String");
4285            Contract.notNull(defValue, "Optional action attribute \"%s\" requires a non-null default value", toString());
4286            Contract.checkArg(!defValue.equals(""), "Optional action attribute \"%s\" requires something more descriptive than an empty String", toString());
4287
4288            this.idvString = idvString; 
4289            this.defaultValue = defValue; 
4290            this.required = (defaultValue.equals("")); 
4291        }
4292
4293        /**
4294         * @return The {@link String} representation of this attribute, as is 
4295         * used by the IDV.
4296         * 
4297         * @see IdvUIManager#ATTR_ACTION
4298         * @see IdvUIManager#ATTR_DESCRIPTION
4299         * @see IdvUIManager#ATTR_GROUP
4300         * @see IdvUIManager#ATTR_ID
4301         * @see IdvUIManager#ATTR_IMAGE
4302         */
4303        public String asIdvString() { return idvString; }
4304
4305        /**
4306         * @return {@literal "Default value"} for this attribute. 
4307         * Blank {@link String}s imply that the attribute is required (and 
4308         * thus lacks a true default value).
4309         */
4310        public String defaultValue() { return defaultValue; }
4311
4312        /**
4313         * @return Whether or not this attribute is a required attribute for 
4314         * valid {@link IdvAction}s.
4315         */
4316        public boolean isRequired() { return required; }
4317    }
4318
4319    /**
4320     * Represents the set of known {@link IdvAction IdvActions} in an idiom
4321     * that can be easily used by both the IDV and McIDAS-V.
4322     */
4323    // TODO(jon:101): use Sets instead of maps and whatnot
4324    // TODO(jon:103): create an invalid IdvAction
4325    public static final class IdvActions {
4326
4327        /** Maps {@literal "id"} values to {@link IdvAction IdvActions}. */
4328        private final Map<String, IdvAction> idToAction =
4329            new ConcurrentHashMap<>();
4330
4331        /**
4332         * Collects {@link IdvAction IdvActions} {@literal "under"} common
4333         * group values.
4334         */
4335        // TODO(jon:102): this should probably become concurrency-friendly.
4336        private final Map<String, Set<IdvAction>> groupToActions =
4337            new LinkedHashMap<>();
4338
4339        /**
4340         * Creates an object that represents the application's
4341         * {@link IdvAction IdvActions}.
4342         * 
4343         * @param idv Reference to the IDV {@literal "god"} object.
4344         *            Cannot be {@code null}.
4345         * @param collectionId IDV resource collection that contains our
4346         *                     actions. Cannot be {@code null}.
4347         * 
4348         * @throws NullPointerException if {@code idv} or {@code collectionId} 
4349         * is {@code null}. 
4350         */
4351        public IdvActions(final IntegratedDataViewer idv, final XmlIdvResource collectionId) {
4352            requireNonNull(idv, "Cannot provide a null IDV reference");
4353            requireNonNull(collectionId, "Cannot build actions from a null collection id");
4354
4355            // TODO(jon): benchmark use of xpath
4356            String query = "//action[@id and @image and @description and @action]";
4357            for (Element e : elements(idv, collectionId, query)) {
4358                IdvAction a = new IdvAction(e);
4359                String id = a.getAttribute(ActionAttribute.ID);
4360                idToAction.put(id, a);
4361                String group = a.getAttribute(ActionAttribute.GROUP);
4362                if (!groupToActions.containsKey(group)) {
4363                    groupToActions.put(group, new LinkedHashSet<>());
4364                }
4365                Set<IdvAction> groupedIds = groupToActions.get(group);
4366                groupedIds.add(a);
4367            }
4368        }
4369
4370        /**
4371         * Attempts to return the {@link IdvAction} associated with the given
4372         * {@code actionId}.
4373         * 
4374         * @param actionId Identifier to use in the search. Cannot be 
4375         * {@code null}.
4376         * 
4377         * @return Either the {@code IdvAction} that matches {@code actionId} 
4378         * or {@code null} if there was no match.
4379         * 
4380         * @throws NullPointerException if {@code actionId} is {@code null}.
4381         */
4382        // TODO(jon:103) here
4383        public IdvAction getAction(final String actionId) {
4384            requireNonNull(actionId, "Null action identifiers are not allowed");
4385            return idToAction.get(actionId);
4386        }
4387
4388        /**
4389         * Searches for the action associated with {@code actionId} and 
4390         * returns the value associated with the given {@link ActionAttribute}.
4391         * 
4392         * @param actionId Identifier to search for. Cannot be {@code null}.
4393         * @param attr Attribute whose value is desired. Cannot be {@code null}.
4394         * 
4395         * @return Either the desired attribute value of the desired action, 
4396         * or {@code null} if {@code actionId} has no associated action.
4397         * 
4398         * @throws NullPointerException if either {@code actionId} or 
4399         * {@code attr} is {@code null}.
4400         */
4401        // TODO(jon:103) here
4402        public String getAttributeForAction(final String actionId, final ActionAttribute attr) {
4403            requireNonNull(actionId, "Null action identifiers are not allowed");
4404            requireNonNull(attr, "Actions cannot have values associated with a null attribute");
4405            IdvAction action = idToAction.get(actionId);
4406            if (action == null) {
4407                return null;
4408            }
4409            return action.getAttribute(attr);
4410        }
4411
4412        /**
4413         * Attempts to return the XML {@link Element} that
4414         * {@literal "represents"} the action associated with {@code actionId}.
4415         * 
4416         * @param actionId Identifier whose XML element is desired.
4417         *                 Cannot be {@code null}.
4418         * 
4419         * @return Either the XML element associated with {@code actionId} or
4420         * {@code null}.
4421         * 
4422         * @throws NullPointerException if {@code actionId} is {@code null}.
4423         * 
4424         * @see IdvAction#originalElement
4425         */
4426        // TODO(jon:103) here
4427        public Element getElementForAction(final String actionId) {
4428            requireNonNull(actionId, "Cannot search for a null action identifier");
4429            IdvAction action = idToAction.get(actionId);
4430            if (action == null) {
4431                return null;
4432            }
4433            return action.getElement();
4434        }
4435
4436        /**
4437         * Attempts to return an {@link Icon} for a given {@link ActionAttribute#ID} and
4438         * {@link ToolbarStyle}.
4439         * 
4440         * @param actionId ID of the action whose {@literal "styled"} icon is 
4441         * desired. Cannot be {@code null}.
4442         * @param style Desired {@code Icon} style. Cannot be {@code null}.
4443         * 
4444         * @return Either the {@code Icon} associated with {@code actionId} 
4445         * and {@code style}, or {@code null}.
4446         * 
4447         * @throws NullPointerException if either {@code actionId} or 
4448         * {@code style} is {@code null}.
4449         */
4450        // TODO(jon:103) here
4451        public Icon getStyledIconFor(final String actionId, final ToolbarStyle style) {
4452            requireNonNull(actionId, "Cannot get an icon for a null action identifier");
4453            requireNonNull(style, "Cannot get an icon for a null ToolbarStyle");
4454            IdvAction a = idToAction.get(actionId);
4455            if (a == null) {
4456                return null;
4457            }
4458            return a.getIconForStyle(style);
4459        }
4460
4461        // TODO(jon:105): replace with something better
4462        public List<String> getAttributes(final ActionAttribute attr) {
4463            requireNonNull(attr, "Actions cannot have null attributes");
4464            List<String> attributeList = arrList(idToAction.size());
4465            for (Map.Entry<String, IdvAction> entry : idToAction.entrySet()) {
4466                attributeList.add(entry.getValue().getAttribute(attr));
4467            }
4468            return attributeList;
4469        }
4470
4471        /**
4472         * @return List of all known {@code IdvAction}s.
4473         */
4474        public List<IdvAction> getAllActions() {
4475            return arrList(idToAction.values());
4476        }
4477
4478        /**
4479         * @return List of all known action groupings.
4480         * 
4481         * @see ActionAttribute#GROUP
4482         * @see #getActionsForGroup(String)
4483         */
4484        public List<String> getAllGroups() {
4485            return arrList(groupToActions.keySet());
4486        }
4487
4488        /**
4489         * Returns the {@link Set} of {@link IdvAction}s associated with the 
4490         * given {@code group}.
4491         * 
4492         * @param group Group whose associated actions you want. Cannot be 
4493         * {@code null}.
4494         * 
4495         * @return Collection of {@code IdvAction}s associated with 
4496         * {@code group}. A blank collection is returned if there are no actions
4497         * associated with {@code group}.
4498         * 
4499         * @throws NullPointerException if {@code group} is {@code null}.
4500         * 
4501         * @see ActionAttribute#GROUP
4502         * @see #getAllGroups()
4503         */
4504        public Set<IdvAction> getActionsForGroup(final String group) {
4505            requireNonNull(group, "Actions cannot be associated with a null group");
4506            if (!groupToActions.containsKey(group)) {
4507                return Collections.emptySet();
4508            }
4509            return groupToActions.get(group);
4510        }
4511
4512        /**
4513         * Returns a summary of the known IDV actions. Please note that this 
4514         * format is subject to change, and is not intended for serialization.
4515         * 
4516         * @return String that looks like 
4517         * {@code [IdvActions@HASHCODE: actions=...]}.
4518         */
4519        @Override public String toString() {
4520            return String.format("[IdvActions@%x: actions=%s]", hashCode(), idToAction);
4521        }
4522    }
4523
4524    /**
4525     * Represents an individual IDV action. Should be fairly adaptable to
4526     * unforeseen changes from Unidata?
4527     */
4528    // TODO(jon:106): Implement equals/hashCode so that you can use these in Sets. The only relevant value should be the id, right?
4529    public static final class IdvAction {
4530
4531        /** The XML {@link Element} that represents this IDV action. */
4532        private final Element originalElement;
4533
4534        /** Mapping of (known) XML attributes to values for this individual action. */
4535        private final Map<ActionAttribute, String> attributes;
4536
4537        /** 
4538         * Simple {@literal "cache"} for the different icons this action has
4539         * displayed. This is {@literal "lazy"}, so the cache does not contain
4540         * icons for {@link ToolbarStyle}s that haven't been used. 
4541         */
4542        private final Map<ToolbarStyle, Icon> iconCache =
4543            new ConcurrentHashMap<>();
4544
4545        /**
4546         * Creates a representation of an IDV action using a given
4547         * {@link Element}.
4548         * 
4549         * @param element XML representation of an IDV action.
4550         *                Cannot be {@code null}.
4551         * 
4552         * @throws NullPointerException if {@code element} is {@code null}.
4553         * @throws IllegalArgumentException if {@code element} is not a valid
4554         * IDV action.
4555         * 
4556         * @see UIManager#isValidIdvAction(Element)
4557         */
4558        public IdvAction(final Element element) {
4559            requireNonNull(element, "Cannot build an action from a null element");
4560            // TODO(jon:107): need a way to diagnose what's wrong with the action?
4561            Contract.checkArg(isValidIdvAction(element), "Action lacks required attributes");
4562            originalElement = element;
4563            attributes = actionElementToMap(element);
4564        }
4565
4566        /**
4567         * @return Returns the {@literal "raw"} path to the icon associated 
4568         * with this action. Remember that this is actually a
4569         * {@literal "format string"} and should not be considered a valid path!
4570         * 
4571         * @see #getIconForStyle
4572         */
4573        public String getRawIconPath() {
4574            return attributes.get(ActionAttribute.ICON);
4575        }
4576
4577        /**
4578         * @return Returns the {@link Icon} associated with
4579         * {@link ToolbarStyle#SMALL}.
4580         */
4581        public Icon getMenuIcon() {
4582            return getIconForStyle(ToolbarStyle.SMALL);
4583        }
4584
4585        /**
4586         * Returns the {@link Icon} associated with this action and the given
4587         * {@link ToolbarStyle}.
4588         * 
4589         * @param style {@literal "Style"} of the {@code Icon} to be returned.
4590         * Cannot be {@code null}.
4591         * 
4592         * @return This action's {@code Icon} with {@code style}
4593         * {@literal "applied."}
4594         * 
4595         * @see ActionAttribute#ICON
4596         * @see #iconCache
4597         */
4598        public Icon getIconForStyle(final ToolbarStyle style) {
4599            requireNonNull(style, "Cannot build an icon for a null ToolbarStyle");
4600            if (!iconCache.containsKey(style)) {
4601                String styledPath = String.format(getRawIconPath(), style.getSize());
4602                URL tmp = getClass().getResource(styledPath);
4603                iconCache.put(style, new ImageIcon(Toolkit.getDefaultToolkit().getImage(tmp)));
4604            }
4605            return iconCache.get(style);
4606        }
4607
4608        /**
4609         * @return Returns the identifier of this {@code IdvAction}.
4610         */
4611        public String getId() {
4612            return getAttribute(ActionAttribute.ID);
4613        }
4614
4615        /**
4616         * Representation of this {@code IdvAction} as an
4617         * {@literal "IDV action call"}.
4618         * 
4619         * @return String that is suitable to hand off to the IDV for execution. 
4620         */
4621        public String getCommand() {
4622            return "idv.handleAction('action:"+getAttribute(ActionAttribute.ID)+"')";
4623        }
4624
4625        /**
4626         * Returns the value associated with a given {@link ActionAttribute} 
4627         * for this action.
4628         * 
4629         * @param attr ActionAttribute whose value you want.
4630         *             Cannot be {@code null}.
4631         * 
4632         * @return Value associated with {@code attr}.
4633         * 
4634         * @throws NullPointerException if {@code attr} is {@code null}.
4635         */
4636        public String getAttribute(final ActionAttribute attr) {
4637            requireNonNull(attr, "No values can be associated with a null ActionAttribute");
4638            return attributes.get(attr);
4639        }
4640
4641        /**
4642         * @return The XML {@link Element} used to create this {@code IdvAction}.
4643         */
4644        // TODO(jon:104): any way to copy this element? if so, this can become an immutable class!
4645        public Element getElement() {
4646            return originalElement;
4647        }
4648
4649        /**
4650         * Returns a brief description of this action. Please note that the 
4651         * format is subject to change and is not intended for serialization.
4652         * 
4653         * @return String that looks like
4654         * {@code [IdvAction@HASHCODE: attributes=...]}.
4655         */
4656        @Override public String toString() {
4657            return String.format("[IdvAction@%x: attributes=%s]", hashCode(), attributes);
4658        }
4659    }
4660}