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