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