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