001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2015
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.event.TreeSelectionEvent;
107import javax.swing.event.TreeSelectionListener;
108import javax.swing.text.JTextComponent;
109import javax.swing.tree.DefaultMutableTreeNode;
110import javax.swing.tree.DefaultTreeCellRenderer;
111import javax.swing.tree.DefaultTreeModel;
112import javax.swing.tree.TreeSelectionModel;
113
114import org.slf4j.Logger;
115import org.slf4j.LoggerFactory;
116import org.w3c.dom.Document;
117import org.w3c.dom.Element;
118import org.w3c.dom.NodeList;
119
120import ucar.unidata.data.DataChoice;
121import ucar.unidata.data.DataOperand;
122import ucar.unidata.data.DataSelection;
123import ucar.unidata.data.DataSource;
124import ucar.unidata.data.DataSourceImpl;
125import ucar.unidata.data.DerivedDataChoice;
126import ucar.unidata.data.UserOperandValue;
127import ucar.unidata.geoloc.LatLonPointImpl;
128import ucar.unidata.idv.ControlDescriptor;
129import ucar.unidata.idv.IdvPersistenceManager;
130import ucar.unidata.idv.IdvPreferenceManager;
131import ucar.unidata.idv.IdvResourceManager;
132import ucar.unidata.idv.IntegratedDataViewer;
133import ucar.unidata.idv.SavedBundle;
134import ucar.unidata.idv.ViewManager;
135import ucar.unidata.idv.ViewState;
136import ucar.unidata.idv.IdvResourceManager.XmlIdvResource;
137import ucar.unidata.idv.control.DisplayControlImpl;
138import ucar.unidata.idv.ui.DataControlDialog;
139import ucar.unidata.idv.ui.DataSelectionWidget;
140import ucar.unidata.idv.ui.DataSelector;
141import ucar.unidata.idv.ui.IdvComponentGroup;
142import ucar.unidata.idv.ui.IdvComponentHolder;
143import ucar.unidata.idv.ui.IdvUIManager;
144import ucar.unidata.idv.ui.IdvWindow;
145import ucar.unidata.idv.ui.IdvXmlUi;
146import ucar.unidata.idv.ui.ViewPanel;
147import ucar.unidata.idv.ui.WindowInfo;
148import ucar.unidata.metdata.NamedStationTable;
149import ucar.unidata.ui.ComponentGroup;
150import ucar.unidata.ui.ComponentHolder;
151import ucar.unidata.ui.HttpFormEntry;
152import ucar.unidata.ui.LatLonWidget;
153import ucar.unidata.ui.RovingProgress;
154import ucar.unidata.ui.XmlUi;
155import ucar.unidata.util.GuiUtils;
156import ucar.unidata.util.IOUtil;
157import ucar.unidata.util.LayoutUtil;
158import ucar.unidata.util.LogUtil;
159import ucar.unidata.util.MenuUtil;
160import ucar.unidata.util.Misc;
161import ucar.unidata.util.Msg;
162import ucar.unidata.util.ObjectListener;
163import ucar.unidata.util.PatternFileFilter;
164import ucar.unidata.util.StringUtil;
165import ucar.unidata.util.TwoFacedObject;
166import ucar.unidata.xml.XmlResourceCollection;
167import ucar.unidata.xml.XmlUtil;
168import edu.wisc.ssec.mcidasv.Constants;
169import edu.wisc.ssec.mcidasv.McIDASV;
170import edu.wisc.ssec.mcidasv.PersistenceManager;
171import edu.wisc.ssec.mcidasv.StateManager;
172import edu.wisc.ssec.mcidasv.supportform.McvStateCollector;
173import edu.wisc.ssec.mcidasv.supportform.SupportForm;
174import edu.wisc.ssec.mcidasv.util.CollectionHelpers;
175import edu.wisc.ssec.mcidasv.util.Contract;
176import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
177import edu.wisc.ssec.mcidasv.util.MemoryMonitor;
178
179/**
180 * <p>Derive our own UI manager to do some specific things:
181 * <ul>
182 *   <li>Removing displays</li>
183 *   <li>Showing the dashboard</li>
184 *   <li>Adding toolbar customization options</li>
185 *   <li>Implement the McIDAS-V toolbar as a JToolbar.</li>
186 *   <li>Deal with bundles without component groups.</li>
187 * </ul></p>
188 */
189// TODO: investigate moving similar unpersisting code to persistence manager.
190public class UIManager extends IdvUIManager implements ActionListener {
191
192    private static final Logger logger = LoggerFactory.getLogger(UIManager.class);
193    
194    /** Id of the "New Display Tab" menu item for the file menu */
195    public static final String MENU_NEWDISPLAY_TAB = "file.new.display.tab";
196
197    /** The tag in the xml ui for creating the special example chooser */
198    public static final String TAG_EXAMPLECHOOSER = "examplechooser";
199
200    /**
201     * Used to keep track of ViewManagers inside a bundle.
202     */
203    public static final HashMap<String, ViewManager> savedViewManagers =
204        new HashMap<>();
205
206    /** 
207     * Property name for whether or not the description field of the support
208     * form should perform line wrapping.
209     * */
210    public static final String PROP_WRAP_SUPPORT_DESC = 
211        "mcidasv.supportform.wrap";
212
213    /** Action command for manipulating the size of the toolbar icons. */
214    private static final String ACT_ICON_TYPE = "action.toolbar.seticonsize";
215
216    /** Action command for removing all displays */
217    private static final String ACT_REMOVE_DISPLAYS = "action.displays.remove";
218
219    /** Action command for showing the dashboard */
220    private static final String ACT_SHOW_DASHBOARD = "action.dashboard.show";
221
222    /** Action command for showing the dashboard */
223    private static final String ACT_SHOW_DATASELECTOR = "action.dataselector.show";
224
225    /** Action command for showing the dashboard */
226    private static final String ACT_SHOW_DISPLAYCONTROLLER = "action.displaycontroller.show";
227
228    /** Action command for displaying the toolbar preference tab. */
229    private static final String ACT_SHOW_PREF = "action.toolbar.showprefs";
230
231    /** Message shown when an unknown action is in the toolbar. */
232    private static final String BAD_ACTION_MSG = "Unknown action (%s) found in your toolbar. McIDAS-V will continue to load, but there will be no button associated with %s.";
233
234    /** Menu ID for the {@literal "Restore Saved Views"} submenu. */
235    public static final String MENU_NEWVIEWS = "menu.tools.projections.restoresavedviews";
236
237    /** Label for the "link" to the toolbar customization preference tab. */
238    private static final String LBL_TB_EDITOR = "Customize...";
239
240    /** Current representation of toolbar actions. */
241    private ToolbarStyle currentToolbarStyle = 
242        getToolbarStylePref(ToolbarStyle.MEDIUM);
243
244    /** The IDV property that reflects the size of the icons. */
245    private static final String PROP_ICON_SIZE = "mcv.ui.iconsize";
246
247    /** The URL of the script that processes McIDAS-V support requests. */
248    private static final String SUPPORT_REQ_URL = 
249        "https://www.ssec.wisc.edu/mcidas/misc/mc-v/supportreq/support.php";
250
251    /** Separator to use between window title components. */
252    protected static final String TITLE_SEPARATOR = " - ";
253
254    /**
255     * The currently "displayed" actions. Keeping this List allows us to get
256     * away with only reading the XML files upon starting the application and 
257     * only writing the XML files upon exiting the application. This will avoid
258     * those redrawing delays.
259     */
260    private List<String> cachedButtons;
261
262    /** Stores all available actions. */
263    private final IdvActions idvActions;
264
265    /** Map of skin ids to their skin resource index. */
266    private Map<String, Integer> skinIds = readSkinIds();
267
268    /** An easy way to figure out who is holding a given ViewManager. */
269    private Map<ViewManager, ComponentHolder> viewManagers = new HashMap<>();
270
271    private int componentHolderCount;
272    
273    private int componentGroupCount;
274    
275    /** Cache for the results of {@link #getWindowTitleFromSkin(int)}. */
276    private final Map<Integer, String> skinToTitle = new ConcurrentHashMap<>();
277
278    /** Maps menu IDs to {@link JMenu}s. */
279//    private Hashtable<String, JMenu> menuIds;
280    private Hashtable<String, JMenuItem> menuIds;
281
282    /** The splash screen (minus easter egg). */
283    private McvSplash splash;
284
285    /** 
286     * A list of the toolbars that the IDV is playing with. Used to apply 
287     * changes to *all* the toolbars in the application.
288     */
289    private List<JToolBar> toolbars;
290
291    /**
292     * Keeping the reference to the toolbar menu mouse listener allows us to
293     * avoid constantly rebuilding the menu. 
294     */
295    private MouseListener toolbarMenu;
296
297    /** Keep the dashboard around so we don't have to re-create it each time. */
298    protected IdvWindow dashboard;
299
300    /** False until {@link #initDone()}. */
301    protected boolean initDone = false;
302
303    /** IDV instantiation--nice to keep around to reduce getIdv() calls. */
304    private IntegratedDataViewer idv;
305
306    /**
307     * Hands off our IDV instantiation to IdvUiManager.
308     *
309     * @param idv The idv
310     */
311    public UIManager(IntegratedDataViewer idv) {
312        super(idv);
313
314        this.idv = idv;
315
316        // cache the appropriate data for the toolbar. it'll make updates 
317        // much snappier
318        idvActions = new IdvActions(getIdv(), IdvResourceManager.RSC_ACTIONS);
319        cachedButtons = readToolbar();
320    }
321
322    /**
323     * Override the IDV method so that we hide component group button.
324     */
325    @Override public IdvWindow createNewWindow(List viewManagers,
326        boolean notifyCollab, String title, String skinPath, Element skinRoot,
327        boolean show, WindowInfo windowInfo) 
328    {
329
330        if (windowInfo != null) {
331            logger.trace("creating window: title='{}' bounds: {}", title, windowInfo.getBounds());
332        } else {
333            logger.trace("creating window: title='{}' bounds: (no windowinfo)", title);
334        }
335        
336        if (Constants.DATASELECTOR_NAME.equals(title)) {
337            show = false;
338        }
339        if (skinPath.indexOf("dashboard.xml") >= 0) {
340            show = false;
341        }
342
343        // used to force any new "display" windows to be the same size as the current window.
344        IdvWindow previousWindow = IdvWindow.getActiveWindow();
345
346        IdvWindow w = super.createNewWindow(viewManagers, notifyCollab, title, 
347            skinPath, skinRoot, show, windowInfo);
348
349        String iconPath = idv.getProperty(Constants.PROP_APP_ICON, (String)null);
350        ImageIcon icon = GuiUtils.getImageIcon(iconPath, getClass(), true);
351        w.setIconImage(icon.getImage());
352
353        // try to catch the dashboard
354        if (Constants.DATASELECTOR_NAME.equals(w.getTitle())) {
355            setDashboard(w);
356        } else if (!w.getComponentGroups().isEmpty()) {
357            // otherwise we need to hide the component group header and explicitly
358            // set the size of the window.
359            ((ComponentHolder)w.getComponentGroups().get(0)).setShowHeader(false);
360            if (previousWindow != null) {
361                Rectangle r = previousWindow.getBounds();
362                
363                w.setBounds(new Rectangle(r.x, r.y, r.width, r.height));
364            }
365        } else {
366            logger.trace("creating window with no component groups");
367        }
368
369        initDisplayShortcuts(w);
370
371        RovingProgress progress =
372            (RovingProgress)w.getComponent(IdvUIManager.COMP_PROGRESSBAR);
373
374        if (progress != null) {
375            progress.start();
376        }
377        return w;
378    }
379
380    /**
381     * Create the display window described by McIDAS-V's default display skin
382     * 
383     * @return {@link IdvWindow} that was created.
384     */
385    public IdvWindow buildDefaultSkin() {
386        return createNewWindow(new ArrayList(), false);
387    }
388
389    /**
390     * Create a new IdvWindow for the given viewManager. Put the
391     * contents of the viewManager into the window
392     * 
393     * @return The new window
394     */
395    public IdvWindow buildEmptyWindow() {
396        Element root = null;
397        String path = null;
398        String skinName = null;
399
400        path = getIdv().getProperty("mcv.ui.emptycompgroup", (String)null);
401        if (path != null) {
402            path = path.trim();
403        }
404
405        if ((path != null) && (path.length() > 0)) {
406            try {
407                root = XmlUtil.getRoot(path, getClass());
408                skinName = getStateManager().getTitle();
409                String tmp = XmlUtil.getAttribute(root, "name", (String)null);
410                if (tmp == null) {
411                    tmp = IOUtil.stripExtension(IOUtil.getFileTail(path));
412                }
413                skinName = skinName + " - " + tmp;
414            } catch (Exception exc) {
415                logger.error("error building empty window", exc);
416            }
417        }
418
419        IdvWindow window = createNewWindow(new ArrayList(), false, skinName, path, root, true, null);
420        window.setVisible(true);
421        return window;
422    }
423
424    
425    /**
426     * Sets {@link #dashboard} to {@code window}. This method also adds some
427     * listeners to {@code window} so that the state of the dashboard is 
428     * automatically saved.
429     * 
430     * @param window The dashboard. Nothing happens if {@link #dashboard} has 
431     * already been set, or this parameter is {@code null}.
432     */
433    private void setDashboard(final IdvWindow window) {
434        if (window == null || dashboard != null)
435            return;
436
437        dashboard = window;
438        dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
439
440        final Component comp = dashboard.getComponent();
441        final ucar.unidata.idv.StateManager state = getIdv().getStateManager();
442
443        // for some reason the component listener's "componentHidden" method
444        // would not fire the *first* time the dashboard is closed/hidden.
445        // the window listener catches it.
446        dashboard.addWindowListener(new WindowListener() {
447            public void windowClosed(final WindowEvent e) {
448                Boolean saveViz = (Boolean)state.getPreference(Constants.PREF_SAVE_DASHBOARD_VIZ, Boolean.FALSE);
449                if (saveViz)
450                    state.putPreference(Constants.PROP_SHOWDASHBOARD, false);
451            }
452
453            public void windowActivated(final WindowEvent e) { }
454            public void windowClosing(final WindowEvent e) { }
455            public void windowDeactivated(final WindowEvent e) { }
456            public void windowDeiconified(final WindowEvent e) { }
457            public void windowIconified(final WindowEvent e) { }
458            public void windowOpened(final WindowEvent e) { }
459        });
460
461        dashboard.getComponent().addComponentListener(new ComponentListener() {
462            public void componentMoved(final ComponentEvent e) {
463                state.putPreference(Constants.PROP_DASHBOARD_BOUNDS, comp.getBounds());
464            }
465
466            public void componentResized(final ComponentEvent e) {
467                state.putPreference(Constants.PROP_DASHBOARD_BOUNDS, comp.getBounds());
468            }
469
470            public void componentShown(final ComponentEvent e) { 
471                Boolean saveViz = (Boolean)state.getPreference(Constants.PREF_SAVE_DASHBOARD_VIZ, Boolean.FALSE);
472                if (saveViz)
473                    state.putPreference(Constants.PROP_SHOWDASHBOARD, true);
474            }
475
476            public void componentHidden(final ComponentEvent e) {
477                Boolean saveViz = (Boolean)state.getPreference(Constants.PREF_SAVE_DASHBOARD_VIZ, Boolean.FALSE);
478                if (saveViz)
479                    state.putPreference(Constants.PROP_SHOWDASHBOARD, false);
480            }
481        });
482
483        Rectangle bounds = (Rectangle)state.getPreferenceOrProperty(Constants.PROP_DASHBOARD_BOUNDS);
484        if (bounds != null)
485            comp.setBounds(bounds);
486    }
487
488    /**
489     * Attempts to add all component holders in {@code info} to
490     * {@code group}. Especially useful when unpersisting a bundle and
491     * attempting to deal with its component groups.
492     * 
493     * @param info The window we want to process.
494     * @param group Receives the holders in {@code info}.
495     * 
496     * @return True if there were component groups in {@code info}.
497     */
498    public boolean unpersistComponentGroups(final WindowInfo info,
499        final McvComponentGroup group) {
500        Collection<Object> comps = info.getPersistentComponents().values();
501
502        if (comps.isEmpty()) {
503            return false;
504        }
505
506        for (Object comp : comps) {
507            // comp is typically always an IdvComponentGroup, but there are
508            // no guarantees...
509            if (! (comp instanceof IdvComponentGroup)) {
510                System.err.println("DEBUG: non IdvComponentGroup found in persistent components: "
511                                   + comp.getClass().getName());
512                continue;
513            }
514
515            IdvComponentGroup bundleGroup = (IdvComponentGroup)comp;
516
517            // need to make a copy of this list to avoid a rogue
518            // ConcurrentModificationException
519            // TODO: determine which threads are clobbering each other.
520            List<IdvComponentHolder> holders = 
521                new ArrayList<>(bundleGroup.getDisplayComponents());
522
523            for (IdvComponentHolder holder : holders) {
524                group.quietAddComponent(holder);
525            }
526
527            group.redoLayout();
528
529            JComponent groupContents = group.getContents();
530            groupContents.setPreferredSize(groupContents.getSize());
531        }
532        return true;
533    }
534
535    /**
536     * Override IdvUIManager's loadLookAndFeel so that we can force the IDV to
537     * load the Aqua look and feel if requested from the command line.
538     */
539    @Override public void loadLookAndFeel() {
540        if (McIDASV.useAquaLookAndFeel) {
541            // since we must rely on the IDV to do the actual loading (due to
542            // our UIManager's name conflicting with javax.swing.UIManager's
543            // name), save the user's preference, replace it temporarily and
544            // have the IDV do its thing, then overwrite the temp preference
545            // with the saved preference. Blah!
546            String previousLF = getStore().get(PREF_LOOKANDFEEL, (String)null);
547            getStore().put(PREF_LOOKANDFEEL, "apple.laf.AquaLookAndFeel");
548            super.loadLookAndFeel();
549            getStore().put(PREF_LOOKANDFEEL, previousLF);
550
551            // locale code was taken out of IDVUIManager's "loadLookAndFeel".
552            //
553            // if the locale preference is set to "system", there will *NOT*
554            // be a PREF_LOCALE -> "Locale.EXAMPLE" key/value pair in main.xml;
555            // as you're using the default state of the preference.
556            // this also means that if there is a non-null value associated
557            // with PREF_LOCALE, the user has selected the US locale.
558            String locale = getStore().get(PREF_LOCALE, (String)null);
559            if (locale != null) {
560                Locale.setDefault(Locale.US);
561            }
562        } else {
563            super.loadLookAndFeel();
564        }
565    }
566
567    @Override public void handleWindowActivated(final IdvWindow window) {
568        List<ViewManager> viewManagers = window.getViewManagers();
569        ViewManager newActive = null;
570        long lastActivatedTime = -1;
571        
572        for (ViewManager viewManager : viewManagers) {
573            if (viewManager.getContents() == null)
574                continue;
575            
576            if (!viewManager.getContents().isVisible())
577                continue;
578            
579            lastActiveFrame = window;
580            
581            if (viewManager.getLastTimeActivated() > lastActivatedTime) {
582                newActive = viewManager;
583                lastActivatedTime = viewManager.getLastTimeActivated();
584            }
585        }
586        
587        if (newActive != null)
588            getVMManager().setLastActiveViewManager(newActive);
589    }
590    
591    /**
592     * Handles the windowing portions of bundle loading: wraps things in
593     * component groups (if needed), merges things into existing windows or
594     * creates new windows, and removes displays and data if asked nicely.
595     * 
596     * @param windows WindowInfos from the bundle.
597     * @param newViewManagers ViewManagers stored in the bundle.
598     * @param okToMerge Put bundled things into an existing window?
599     * @param fromCollab Did this come from the collab stuff?
600     * @param didRemoveAll Remove all data and displays?
601     * 
602     * @see IdvUIManager#unpersistWindowInfo(List, List, boolean, boolean,
603     *      boolean)
604     */
605    @Override public void unpersistWindowInfo(List windows,
606            List newViewManagers, boolean okToMerge, boolean fromCollab,
607            boolean didRemoveAll) 
608        {
609            if (newViewManagers == null) {
610                newViewManagers = new ArrayList<>();
611            }
612
613            // keep track of the "old" state if the user wants to remove things.
614            boolean mergeLayers = ((PersistenceManager)getPersistenceManager()).getMergeBundledLayers();
615            List<IdvComponentHolder> holdersBefore = new ArrayList<>();
616            List<IdvWindow> windowsBefore = new ArrayList<>();
617            if (didRemoveAll) {
618                holdersBefore.addAll(McVGuiUtils.getAllComponentHolders());
619                windowsBefore.addAll(McVGuiUtils.getAllDisplayWindows());
620            }
621
622            for (WindowInfo info : (List<WindowInfo>)windows) {
623                newViewManagers.removeAll(info.getViewManagers());
624                makeBundledDisplays(info, okToMerge, mergeLayers, fromCollab);
625
626                if (mergeLayers) {
627                    holdersBefore.addAll(McVGuiUtils.getComponentHolders(info));
628                }
629            }
630//            System.err.println("holdersBefore="+holdersBefore);
631            // no reason to kill the displays if there aren't any windows in the
632            // bundle!
633            if ((mergeLayers) || (didRemoveAll && !windows.isEmpty())) {
634                killOldDisplays(holdersBefore, windowsBefore, (okToMerge || mergeLayers));
635            }
636        }
637
638    /**
639     * Removes data and displays that existed prior to loading a bundle.
640     * 
641     * @param oldHolders Component holders around before loading.
642     * @param oldWindows Windows around before loading.
643     * @param merge Were the bundle contents merged into an existing window?
644     */
645    public void killOldDisplays(final List<IdvComponentHolder> oldHolders,
646        final List<IdvWindow> oldWindows, final boolean merge) 
647    {
648//        System.err.println("killOldDisplays: merge="+merge);
649        // if we merged, this will ensure that any old holders in the merged
650        // window also get removed.
651        if (merge) {
652            for (IdvComponentHolder holder : oldHolders) {
653                holder.doRemove();
654            }
655        }
656
657        // mop up any windows that no longer have component holders.
658        for (IdvWindow window : oldWindows) {
659            IdvComponentGroup group = McVGuiUtils.getComponentGroup(window);
660
661            List<IdvComponentHolder> holders =
662                McVGuiUtils.getComponentHolders(group);
663
664            // if the old set of holders contains all of this window's
665            // holders, this window can be deleted:
666            // 
667            // this works fine for merging because the okToMerge stuff will
668            // remove all old holders from the current window, but if the
669            // bundle was merged into this window, containsAll() will fail
670            // due to there being a new holder.
671            // 
672            // if the bundle was loaded into its own window, then
673            // all the old windows will pass this test.
674            if (oldHolders.containsAll(holders)) {
675                group.doRemove();
676                window.dispose();
677            }
678        }
679    }
680    
681
682    /**
683     * A hack because Unidata moved the skins (taken from 
684     * {@link IdvPersistenceManager}).
685     * 
686     * @param skinPath original path
687     * @return fixed path
688     */
689    private String fixSkinPath(String skinPath) {
690        if (skinPath == null) {
691            return null;
692        }
693        if (StringUtil.stringMatch(
694                skinPath, "^/ucar/unidata/idv/resources/[^/]+\\.xml")) {
695            skinPath =
696                StringUtil.replace(skinPath, "/ucar/unidata/idv/resources/",
697                                   "/ucar/unidata/idv/resources/skins/");
698        }
699        return skinPath;
700    }
701    
702    /**
703     * <p>
704     * Uses the contents of {@code info} to rebuild a display that has been 
705     * bundled. If {@code merge} is true, the displayable parts of the bundle 
706     * will be put into the current window. Otherwise a new window is created 
707     * and the relevant parts of the bundle will occupy that new window.
708     * </p>
709     * 
710     * @param info WindowInfo to use with creating the new window.
711     * @param merge Merge created things into an existing window?
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(new Runnable() {
903            public void run() {
904                new AboutFrame((McIDASV)idv).setVisible(true);
905            }
906        });
907    }
908
909    /**
910     * Handles all the ActionEvents that occur for widgets contained within
911     * this class. It's not so pretty, but it isolates the event handling in
912     * one place (and reduces the number of action listeners to one).
913     * 
914     * @param e The event that triggered the call to this method.
915     */
916    public void actionPerformed(ActionEvent e) {
917        String cmd = (String)e.getActionCommand();
918        boolean toolbarEditEvent = false;
919
920        // handle selecting large icons
921        if (cmd.startsWith(ToolbarStyle.LARGE.getAction())) {
922            currentToolbarStyle = ToolbarStyle.LARGE;
923            toolbarEditEvent = true;
924        }
925
926        // handle selecting medium icons
927        else if (cmd.startsWith(ToolbarStyle.MEDIUM.getAction())) {
928            currentToolbarStyle = ToolbarStyle.MEDIUM;
929            toolbarEditEvent = true;
930        }
931
932        // handle selecting small icons
933        else if (cmd.startsWith(ToolbarStyle.SMALL.getAction())) {
934            currentToolbarStyle = ToolbarStyle.SMALL;
935            toolbarEditEvent = true;
936        }
937
938        // handle the user selecting the show toolbar preference menu item
939        else if (cmd.startsWith(ACT_SHOW_PREF)) {
940            IdvPreferenceManager prefs = idv.getPreferenceManager();
941            prefs.showTab(Constants.PREF_LIST_TOOLBAR);
942            toolbarEditEvent = true;
943        }
944
945        // handle the user toggling the size of the icon
946        else if (cmd.startsWith(ACT_ICON_TYPE))
947            toolbarEditEvent = true;
948
949        // handle the user removing displays
950        else if (cmd.startsWith(ACT_REMOVE_DISPLAYS))
951            idv.removeAllDisplays();
952
953        // handle popping up the dashboard.
954        else if (cmd.startsWith(ACT_SHOW_DASHBOARD))
955            showDashboard();
956
957        // handle popping up the data explorer.
958        else if (cmd.startsWith(ACT_SHOW_DATASELECTOR))
959            showDashboard("Data Sources");
960
961        // handle popping up the display controller.
962        else if (cmd.startsWith(ACT_SHOW_DISPLAYCONTROLLER))
963            showDashboard("Layer Controls");
964
965        else
966            System.err.println("Unsupported action event!");
967
968        // if the user did something to change the toolbar, hide the current
969        // toolbar, replace it, and then make the new toolbar visible.
970        if (toolbarEditEvent == true) {
971
972            getStateManager().writePreference(PROP_ICON_SIZE, 
973                currentToolbarStyle.getSizeAsString());
974
975            // destroy the menu so it can be properly updated during rebuild
976            toolbarMenu = null;
977
978            for (JToolBar toolbar : toolbars) {
979                toolbar.setVisible(false);
980                populateToolbar(toolbar);
981                toolbar.setVisible(true);
982            }
983        }
984    }
985
986    public JComponent getDisplaySelectorComponent() {
987        DefaultMutableTreeNode root = new DefaultMutableTreeNode("");
988        DefaultTreeModel model = new DefaultTreeModel(root);
989        final JTree tree = new JTree(model);
990        tree.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
991        tree.getSelectionModel().setSelectionMode(
992            TreeSelectionModel.SINGLE_TREE_SELECTION
993        );
994        DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
995        renderer.setIcon(null);
996        renderer.setOpenIcon(null);
997        renderer.setClosedIcon(null);
998        tree.setCellRenderer(renderer);
999
1000        for (IdvWindow w : McVGuiUtils.getAllDisplayWindows()) {
1001            String title = w.getTitle();
1002            TwoFacedObject winTFO = new TwoFacedObject(title, w);
1003            DefaultMutableTreeNode winNode = new DefaultMutableTreeNode(winTFO);
1004            for (IdvComponentHolder h : McVGuiUtils.getComponentHolders(w)) {
1005                String hName = h.getName();
1006                TwoFacedObject tmp = new TwoFacedObject(hName, h);
1007                DefaultMutableTreeNode holderNode = new DefaultMutableTreeNode(tmp);
1008                //for (ViewManager v : (List<ViewManager>)h.getViewManagers()) {
1009                for (int i = 0; i < h.getViewManagers().size(); i++) {
1010                    ViewManager v = (ViewManager)h.getViewManagers().get(i);
1011                    String vName = v.getName();
1012                    TwoFacedObject tfo = null;
1013                    
1014                    if (vName != null && vName.length() > 0)
1015                        tfo = new TwoFacedObject(vName, v);
1016                    else
1017                        tfo = new TwoFacedObject(Constants.PANEL_NAME + " " + (i+1), v);
1018                    
1019                    holderNode.add(new DefaultMutableTreeNode(tfo));
1020                }
1021                winNode.add(holderNode);
1022            }
1023            root.add(winNode);
1024        }
1025
1026        // select the appropriate view
1027        tree.addTreeSelectionListener(new TreeSelectionListener() {
1028            public void valueChanged(TreeSelectionEvent evt) {
1029                DefaultMutableTreeNode node = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();
1030                if (node == null || !(node.getUserObject() instanceof TwoFacedObject)) {
1031                    return;
1032                }
1033                TwoFacedObject tfo = (TwoFacedObject) node.getUserObject();
1034
1035                Object obj = tfo.getId();
1036                if (obj instanceof ViewManager) {
1037                    ViewManager viewManager = (ViewManager) tfo.getId();
1038                    idv.getVMManager().setLastActiveViewManager(viewManager);
1039                } else if (obj instanceof McvComponentHolder) {
1040                    McvComponentHolder holder = (McvComponentHolder)obj;
1041                    holder.setAsActiveTab();
1042                } else if (obj instanceof IdvWindow) {
1043                    IdvWindow window = (IdvWindow)obj;
1044                    window.toFront();
1045                }
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(new ActionListener() {
1386
1387            public void actionPerformed(ActionEvent e) {
1388                // running in a separate thread is kinda nice! *HEH*
1389                Misc.run(UIManager.this, "processBundle", bundle);
1390            }
1391        });
1392        toolbar.add(button);
1393    }
1394
1395    /**
1396     * Handle loading the given {@link ucar.unidata.idv.SavedBundle SavedBundle}.
1397     *
1398     * <p>Overridden in McIDAS-V to allow {@literal "default"} bundles to
1399     * show the same bundle loading dialog as a {@literal "favorite"} bundle.
1400     * </p>
1401     *
1402     * @param bundle Bundle to process. Cannot be {@code null}.
1403     */
1404    @Override public void processBundle(SavedBundle bundle) {
1405        showWaitCursor();
1406        LogUtil.message("Loading bundle: " + bundle.getName());
1407        int type = bundle.getType();
1408        boolean checkToRemove = (type == SavedBundle.TYPE_FAVORITE) || (type == SavedBundle.TYPE_SYSTEM);
1409        boolean ok = getPersistenceManager().decodeXmlFile(bundle.getUrl(),
1410            bundle.getName(),
1411            checkToRemove);
1412        if (ok && (bundle.getType() == SavedBundle.TYPE_DATA)) {
1413            showDataSelector();
1414        }
1415        LogUtil.message("");
1416        showNormalCursor();
1417    }
1418
1419    /**
1420     * Builds two things, given a toolbar and a tree node: a JButton that
1421     * represents a {@literal "first-level"} parent node and a JPopupMenu that
1422     * appears upon clicking the JButton. The button is then added to the given
1423     * toolbar.
1424     * 
1425     * <p>{@literal "First-level"} means the given node is a child of the root
1426     * node.</p>
1427     * 
1428     * @param toolbar Toolbar to which we add the bundle tree.
1429     * @param node Node we want to add.
1430     */
1431    private void addBundleTree(JToolBar toolbar, BundleTreeNode node) {
1432        ImageIcon catIcon =
1433            GuiUtils.getImageIcon("/auxdata/ui/icons/Folder.gif");
1434
1435        final JButton button = new JButton(node.getName(), catIcon);
1436        final JPopupMenu popup = new JPopupMenu();
1437
1438        button.setToolTipText("Show Favorites category: " + node.getName());
1439
1440        button.addActionListener(new ActionListener() {
1441            public void actionPerformed(ActionEvent e) {
1442                popup.show(button, 0, button.getHeight());
1443            }
1444        });
1445
1446        toolbar.add(button);
1447
1448        // recurse through the child nodes
1449        for (BundleTreeNode kid : node.getChildren()) {
1450            buildPopupMenu(kid, popup);
1451        }
1452    }
1453
1454    /**
1455     * Writes the currently displayed toolbar buttons to the toolbar XML. This
1456     * has mostly been ripped off from ToolbarEditor. :(
1457     */
1458    public void writeToolbar() {
1459        XmlResourceCollection resources =
1460            getResourceManager()
1461                .getXmlResources(IdvResourceManager.RSC_TOOLBAR);
1462
1463        String actionPrefix = "action:";
1464
1465        // ensure that the IDV can read the XML we're generating.
1466        Document doc = resources.getWritableDocument("<panel/>");
1467        Element root = resources.getWritableRoot("<panel/>");
1468        root.setAttribute(XmlUi.ATTR_LAYOUT, XmlUi.LAYOUT_FLOW);
1469        root.setAttribute(XmlUi.ATTR_MARGIN, "4");
1470        root.setAttribute(XmlUi.ATTR_VSPACE, "0");
1471        root.setAttribute(XmlUi.ATTR_HSPACE, "2");
1472        root.setAttribute(XmlUi.inheritName(XmlUi.ATTR_SPACE), "2");
1473        root.setAttribute(XmlUi.inheritName(XmlUi.ATTR_WIDTH), "5");
1474
1475        // clear out any pesky kids from previous relationships. XML don't need
1476        // no baby-mama drama.
1477        XmlUtil.removeChildren(root);
1478
1479        // iterate through the actions that have toolbar buttons in use and add
1480        // 'em to the XML.
1481        for (String action : cachedButtons) {
1482            Element e;
1483            if (action != null) {
1484                e = doc.createElement(XmlUi.TAG_BUTTON);
1485                e.setAttribute(XmlUi.ATTR_ACTION, (actionPrefix + action));
1486            } else {
1487                e = doc.createElement(XmlUi.TAG_FILLER);
1488                e.setAttribute(XmlUi.ATTR_WIDTH, "5");
1489            }
1490            root.appendChild(e);
1491        }
1492
1493        // write the XML
1494        try {
1495            resources.writeWritable();
1496        } catch (Exception e) {
1497            e.printStackTrace();
1498        }
1499    }
1500
1501    /**
1502     * Read the contents of the toolbar XML into a List. We're essentially just
1503     * throwing actions into the list.
1504     * 
1505     * @return The actions/buttons that live in the toolbar xml. Note that if 
1506     * an element is {@code null}, this element represents a {@literal "space"}
1507     * that should appear in both the Toolbar and the Toolbar Preferences.
1508     */
1509    public List<String> readToolbar() {
1510        final Element root = getToolbarRoot();
1511        if (root == null) {
1512            return null;
1513        }
1514
1515        final NodeList elements = XmlUtil.getElements(root);
1516        List<String> data = new ArrayList<String>(elements.getLength());
1517        for (int i = 0; i < elements.getLength(); i++) {
1518            Element child = (Element)elements.item(i);
1519            if (child.getTagName().equals(XmlUi.TAG_BUTTON)) {
1520                data.add(
1521                    XmlUtil.getAttribute(child, ATTR_ACTION, (String)null)
1522                        .substring(7));
1523            } else {
1524                data.add(null);
1525            }
1526        }
1527        return data;
1528    }
1529
1530    /**
1531     * Returns the icon associated with {@code actionId}. Note that associating
1532     * the {@literal "missing icon"} icon with an action is allowable.
1533     * 
1534     * @param actionId Action ID whose associated icon is to be returned.
1535     * @param style Returned icon's size will be the size associated with the
1536     * specified {@code ToolbarStyle}.
1537     * 
1538     * @return Either the icon corresponding to {@code actionId} or the default
1539     * {@literal "missing icon"} icon.
1540     * 
1541     * @throws NullPointerException if {@code actionId} is null.
1542     */
1543    protected Icon getActionIcon(final String actionId, 
1544        final ToolbarStyle style) 
1545    {
1546        if (actionId == null)
1547            throw new NullPointerException("Action ID cannot be null");
1548
1549        Icon actionIcon = idvActions.getStyledIconFor(actionId, style);
1550        if (actionIcon != null)
1551            return actionIcon;
1552
1553        String icon = "/edu/wisc/ssec/mcidasv/resources/icons/toolbar/range-bearing%d.png";
1554        URL tmp = getClass().getResource(String.format(icon, style.getSize()));
1555        return new ImageIcon(tmp);
1556    }
1557
1558    /**
1559     * Returns the known {@link IdvAction}s in the form of {@link IdvActions}.
1560     * 
1561     * @return {@link #idvActions}
1562     */
1563    public IdvActions getCachedActions() {
1564        return idvActions;
1565    }
1566
1567    /**
1568     * Returns the actions that currently make up the McIDAS-V toolbar.
1569     * 
1570     * @return {@link List} of {@link ActionAttribute#ID}s that make up the
1571     * current toolbar buttons.
1572     */
1573    public List<String> getCachedButtons() {
1574        if (cachedButtons == null) {
1575            cachedButtons = readToolbar();
1576        }
1577        return cachedButtons;
1578    }
1579
1580    /**
1581     * Make the menu of actions.
1582     * 
1583     * <p>Overridden in McIDAS-V so that we can fool the IDV into working with
1584     * our icons that allow for multiple {@literal "styles"}.
1585     * 
1586     * @param obj Object to call.
1587     * @param method Method to call.
1588     * @param makeCall if {@code true}, call 
1589     * {@link IntegratedDataViewer#handleAction(String)}.
1590     * 
1591     * @return List of {@link JMenu}s that represent our action menus.
1592     */
1593    @Override public List<JMenu> makeActionMenu(final Object obj, 
1594        final String method, final boolean makeCall) 
1595    {
1596        List<JMenu> menu = arrList();
1597        IdvActions actions = getCachedActions();
1598        for (String group : actions.getAllGroups()) {
1599            List<JMenuItem> items = arrList();
1600            for (IdvAction action : actions.getActionsForGroup(group)) {
1601                String cmd = (makeCall) ? action.getCommand() : action.getId();
1602                String desc = action.getAttribute(ActionAttribute.DESCRIPTION);
1603//                items.add(GuiUtils.makeMenuItem(desc, obj, method, cmd));
1604                items.add(makeMenuItem(desc, obj, method, cmd));
1605            }
1606//            menu.add(GuiUtils.makeMenu(group, items));
1607            menu.add(makeMenu(group, items));
1608        }
1609        return menu;
1610    }
1611
1612    /**
1613     * @see ucar.unidata.util.MenuUtil#makeMenuItem(String, Object, String, Object)
1614     */
1615    public static JMenuItem makeMenuItem(String label, Object obj, 
1616        String method, Object arg) 
1617    {
1618        return MenuUtil.makeMenuItem(label, obj, method, arg);
1619    }
1620
1621    /**
1622     * @see ucar.unidata.util.MenuUtil#makeMenu(String, List)
1623     */
1624    @SuppressWarnings("unchecked")
1625    public static JMenu makeMenu(String name, List menuItems) {
1626        return MenuUtil.makeMenu(name, menuItems);
1627    }
1628
1629    /**
1630     * Returns the collection of action identifiers.
1631     * 
1632     * <p>Overridden in McIDAS-V so that we can fool the IDV into working with
1633     * our icons that allow for multiple {@literal "styles"}.
1634     * 
1635     * @return {@link List} of {@link String}s that correspond to
1636     * {@link IdvAction IdvActions}.
1637     */
1638    @Override public List<String> getActions() {
1639        return idvActions.getAttributes(ActionAttribute.ID);
1640    }
1641
1642    /**
1643     * Looks for the XML {@link Element} representation of the action 
1644     * associated with {@code actionId}.
1645     * 
1646     * <p>Overridden in McIDAS-V so that we can fool the IDV into working with
1647     * our icons that allow for multiple {@literal "styles"}.
1648     * 
1649     * @param actionId ID of the action whose {@literal "action node"} is desired. Cannot be {@code null}.
1650     * 
1651     * @return {@literal "action node"} associated with {@code actionId}.
1652     * 
1653     * @throws NullPointerException if {@code actionId} is {@code null}.
1654     */
1655    @Override public Element getActionNode(final String actionId) {
1656        requireNonNull(actionId, "Null action id strings are invalid");
1657        return idvActions.getElementForAction(actionId);
1658    }
1659
1660    /**
1661     * Searches for an action identified by a given {@code actionId}, and 
1662     * returns the value associated with its {@code attr}.
1663     * 
1664     * <p>Overridden in McIDAS-V so that we can fool the IDV into working with
1665     * our icons that allow for multiple {@literal "styles"}.
1666     * 
1667     * @param actionId ID of the action whose attribute value is desired. Cannot be {@code null}.
1668     * @param attr The attribute whose value is desired. Cannot be {@code null}.
1669     * 
1670     * @return Value associated with the given action and given attribute.
1671     * 
1672     * @throws NullPointerException if {@code actionId} or {@code attr} is {@code null}.
1673     */
1674    @Override public String getActionAttr(final String actionId, 
1675        final String attr) 
1676    {
1677        requireNonNull(actionId, "Null action id strings are invalid");
1678        requireNonNull(attr, "Null attributes are invalid");
1679        ActionAttribute actionAttr = ActionAttribute.valueOf(attr.toUpperCase());
1680        return idvActions.getAttributeForAction(stripAction(actionId), actionAttr);
1681    }
1682
1683    /**
1684     * Attempts to verify that {@code element} represents a {@literal "valid"}
1685     * IDV action.
1686     * 
1687     * @param element {@link Element} to check. {@code null} values permitted, 
1688     * but they return {@code false}.
1689     * 
1690     * @return {@code true} if {@code element} had all required 
1691     * {@link ActionAttribute}s. {@code false} otherwise, or if 
1692     * {@code element} is {@code null}.
1693     */
1694    private static boolean isValidIdvAction(final Element element) {
1695        if (element == null) {
1696            return false;
1697        }
1698        for (ActionAttribute attribute : ActionAttribute.values()) {
1699            if (!attribute.isRequired()) {
1700                continue;
1701            }
1702            if (!XmlUtil.hasAttribute(element, attribute.asIdvString())) {
1703                return false;
1704            }
1705        }
1706        return true;
1707    }
1708
1709    /**
1710     * Builds a {@link Map} of {@link ActionAttribute}s to values for a given
1711     * {@link Element}. If {@code element} does not contain an optional attribute,
1712     * use the attribute's default value.
1713     * 
1714     * @param element {@literal "Action node"} of interest. {@code null} 
1715     * permitted, but results in an empty {@code Map}.
1716     * 
1717     * @return Mapping of {@code ActionAttribute}s to values, or an empty 
1718     * {@code Map} if {@code element} is {@code null}.
1719     */
1720    private static Map<ActionAttribute, String> actionElementToMap(
1721        final Element element) 
1722    {
1723        if (element == null) {
1724            return Collections.emptyMap();
1725        }
1726        // loop through set of action attributes; if element contains attribute "A", add it; return results.
1727        Map<ActionAttribute, String> attrs = new LinkedHashMap<>();
1728        for (ActionAttribute attribute : ActionAttribute.values()) {
1729            String idvStr = attribute.asIdvString();
1730            if (XmlUtil.hasAttribute(element, idvStr)) {
1731                attrs.put(attribute, XmlUtil.getAttribute(element, idvStr));
1732            } else {
1733                attrs.put(attribute, attribute.defaultValue());
1734            }
1735        }
1736        return attrs;
1737    }
1738
1739    /**
1740     * <p>
1741     * Builds a tree out of the bundles that should appear within the McV
1742     * toolbar. A tree is a nice way to store this data, as the default IDV
1743     * behavior is to act kinda like a file explorer when it displays these
1744     * bundles.
1745     * </p>
1746     * 
1747     * <p>
1748     * The tree makes it REALLY easy to replicate the default IDV
1749     * functionality.
1750     * </p>
1751     *
1752     * @param bundleType One of {@link ucar.unidata.idv.SavedBundle#TYPE_FAVORITE},
1753     * {@link ucar.unidata.idv.SavedBundle#TYPE_DISPLAY},
1754     * {@link ucar.unidata.idv.SavedBundle#TYPE_DATA} or
1755     * {@link ucar.unidata.idv.SavedBundle#TYPE_SYSTEM}.
1756     * 
1757     * @return The root BundleTreeNode for the tree containing toolbar bundles.
1758     */
1759    public BundleTreeNode buildBundleTree(int bundleType) {
1760        final String TOOLBAR = "Toolbar";
1761
1762        final List<SavedBundle> bundles =
1763            getPersistenceManager().getBundles(bundleType);
1764
1765        // handy reference to parent nodes; bundle count * 4 seems pretty safe
1766        final Map<String, BundleTreeNode> mapper = new HashMap<>(bundles.size() * 4);
1767
1768        // iterate through all toolbar bundles
1769        for (SavedBundle bundle : bundles) {
1770            String categoryPath = "";
1771            String grandParentPath;
1772
1773            // build the "path" to the bundle. these paths basically look like
1774            // "Toolbar>category>subcategory>." so "category" is a category of
1775            // toolbar bundles and subcategory is a subcategory of that. The
1776            // IDV will build nice JPopupMenus with everything that appears in
1777            // "category," so McV needs to do the same thing. thus McV needs to
1778            // figure out the complete path to each toolbar bundle!
1779            List<String> categories = (List<String>)bundle.getCategories();
1780            if (categories == null || categories.isEmpty() || !TOOLBAR.equals(categories.get(0))) {
1781                continue;
1782            }
1783
1784            for (String category : categories) {
1785                grandParentPath = categoryPath;
1786                categoryPath += category + '>';
1787
1788                if (!mapper.containsKey(categoryPath)) {
1789                    BundleTreeNode grandParent = mapper.get(grandParentPath);
1790                    BundleTreeNode parent = new BundleTreeNode(category);
1791                    if (grandParent != null) {
1792                        grandParent.addChild(parent);
1793                    }
1794                    mapper.put(categoryPath, parent);
1795                }
1796            }
1797
1798            // so the tree book-keeping (if any) is done and we can just add
1799            // the current SavedBundle to its parent node within the tree.
1800            BundleTreeNode parent = mapper.get(categoryPath);
1801            parent.addChild(new BundleTreeNode(bundle.getName(), bundle));
1802        }
1803
1804        // return the root of the tree.
1805        return mapper.get("Toolbar>");
1806    }
1807    
1808    /**
1809     * Recursively builds the contents of the (first call) JPopupMenu. This is
1810     * where that tree annoyance stuff comes in handy. This is basically a
1811     * simple tree traversal situation.
1812     * 
1813     * @param node The node that we're trying to use to build the contents.
1814     * @param comp The component to which we add node contents.
1815     */
1816    private void buildPopupMenu(BundleTreeNode node, JComponent comp) {
1817        // if the current node has no bundle, it's considered a parent node
1818        if (node.getBundle() == null) {
1819            // parent nodes mean that we have to create a JMenu and add it
1820            JMenu test = new JMenu(node.getName());
1821            comp.add(test);
1822
1823            // recurse through children to continue building.
1824            for (BundleTreeNode kid : node.getChildren()) {
1825                buildPopupMenu(kid, test);
1826            }
1827
1828        } else {
1829            // nodes with bundles can simply be added to the JMenu
1830            // (or JPopupMenu)
1831            JMenuItem mi = new JMenuItem(node.getName());
1832            final SavedBundle theBundle = node.getBundle();
1833
1834            mi.addActionListener(new ActionListener() {
1835                public void actionPerformed(ActionEvent ae) {
1836                    //Do it in a thread
1837                    Misc.run(UIManager.this, "processBundle", theBundle);
1838                }
1839            });
1840
1841            comp.add(mi);
1842        }
1843    }
1844
1845    @Override public void initDone() {
1846        super.initDone();
1847        if (getStore().get(Constants.PREF_VERSION_CHECK, true)) {
1848            StateManager stateManager = (StateManager)getStateManager();
1849            stateManager.checkForNewerVersion(false);
1850            stateManager.checkForNotice(false);
1851        }
1852
1853        // not super excited about how this works.
1854        // showBasicWindow(true);
1855
1856        initDone = true;
1857        showDashboard();
1858    }
1859
1860    /**
1861     * Create the splash screen if needed
1862     */
1863    public void initSplash() {
1864        if (getProperty(PROP_SHOWSPLASH, true)
1865                && !getArgsManager().getNoGui()
1866                && !getArgsManager().getIsOffScreen()
1867                && !getArgsManager().testMode) {
1868            splash = new McvSplash(idv);
1869            splashMsg("Loading Programs");
1870        }
1871    }
1872    
1873    /**
1874     *  Create (if null)  and show the HelpTipDialog. If checkPrefs is true
1875     *  then only create the dialog if the PREF_HELPTIPSHOW preference is true.
1876     *
1877     * @param checkPrefs Should the user preferences be checked
1878     */
1879    /** THe help tip dialog */
1880    private McvHelpTipDialog helpTipDialog;
1881
1882    public void initHelpTips(boolean checkPrefs) {
1883        try {
1884            if (getIdv().getArgsManager().getIsOffScreen()) {
1885                return;
1886            }
1887            if (checkPrefs) {
1888                if (!getStore().get(McvHelpTipDialog.PREF_HELPTIPSHOW, true)) {
1889                    return;
1890                }
1891            }
1892            if (helpTipDialog == null) {
1893                IdvResourceManager resourceManager = getResourceManager();
1894                helpTipDialog = new McvHelpTipDialog(
1895                    resourceManager.getXmlResources(
1896                        resourceManager.RSC_HELPTIPS), getIdv(), getStore(),
1897                            getIdvClass(),
1898                            getStore().get(
1899                                McvHelpTipDialog.PREF_HELPTIPSHOW, true));
1900            }
1901            helpTipDialog.setVisible(true);
1902            GuiUtils.toFront(helpTipDialog);
1903        } catch (Throwable excp) {
1904            logException("Reading help tips", excp);
1905        }
1906    }
1907
1908    /**
1909     *  If created, close the HelpTipDialog window.
1910     */
1911    public void closeHelpTips() {
1912        if (helpTipDialog != null) {
1913            helpTipDialog.setVisible(false);
1914        }
1915    }
1916
1917    /**
1918     *  Create (if null)  and show the HelpTipDialog
1919     */
1920    public void showHelpTips() {
1921        initHelpTips(false);
1922    }
1923
1924    /**
1925     * Populate a menu with bundles known to the {@code PersistenceManager}.
1926     *
1927     * @param inBundleMenu The menu to populate
1928     */
1929    public void makeBundleMenu(JMenu inBundleMenu) {
1930        final int bundleType = IdvPersistenceManager.BUNDLES_FAVORITES;
1931
1932        JMenuItem mi;
1933        mi = new JMenuItem("Manage...");
1934        McVGuiUtils.setMenuImage(mi, Constants.ICON_FAVORITEMANAGE_SMALL);
1935        mi.setMnemonic(GuiUtils.charToKeyCode("M"));
1936        inBundleMenu.add(mi);
1937        mi.addActionListener(new ActionListener() {
1938            public void actionPerformed(ActionEvent ae) {
1939                showBundleDialog(bundleType);
1940            }
1941        });
1942
1943        final List bundles = getPersistenceManager().getBundles(bundleType);
1944        if (bundles.isEmpty()) {
1945            return;
1946        }
1947        final String title =
1948            getPersistenceManager().getBundleTitle(bundleType);
1949        final String bundleDir =
1950            getPersistenceManager().getBundleDirectory(bundleType);
1951
1952        JMenu bundleMenu = new JMenu(title);
1953        McVGuiUtils.setMenuImage(bundleMenu, Constants.ICON_FAVORITE_SMALL);
1954        bundleMenu.setMnemonic(GuiUtils.charToKeyCode(title));
1955
1956//        getPersistenceManager().initBundleMenu(bundleType, bundleMenu);
1957
1958        Hashtable catMenus = new Hashtable();
1959        inBundleMenu.addSeparator();
1960        inBundleMenu.add(bundleMenu);
1961        for (int i = 0; i < bundles.size(); i++) {
1962            SavedBundle bundle       = (SavedBundle) bundles.get(i);
1963            List        categories   = bundle.getCategories();
1964            JMenu       catMenu      = bundleMenu;
1965            String      mainCategory = "";
1966            for (int catIdx = 0; catIdx < categories.size(); catIdx++) {
1967                String category = (String) categories.get(catIdx);
1968                mainCategory += "." + category;
1969                JMenu tmpMenu = (JMenu) catMenus.get(mainCategory);
1970                if (tmpMenu == null) {
1971                    tmpMenu = new JMenu(category);
1972                    catMenu.add(tmpMenu);
1973                    catMenus.put(mainCategory, tmpMenu);
1974                }
1975                catMenu = tmpMenu;
1976            }
1977
1978            final SavedBundle theBundle = bundle;
1979            mi = new JMenuItem(bundle.getName());
1980            mi.addActionListener(new ActionListener() {
1981                public void actionPerformed(ActionEvent ae) {
1982                    //Do it in a thread
1983                    Misc.run(UIManager.this, "processBundle", theBundle);
1984                }
1985            });
1986            catMenu.add(mi);
1987        }
1988    }
1989
1990    /**
1991     * Overridden to build a custom Window menu.
1992     * @see ucar.unidata.idv.ui.IdvUIManager#makeWindowsMenu(JMenu, IdvWindow)
1993     */
1994    @Override public void makeWindowsMenu(final JMenu windowMenu, final IdvWindow idvWindow) {
1995        JMenuItem mi;
1996        boolean first = true;
1997
1998        mi = new JMenuItem("Show Data Explorer");
1999        McVGuiUtils.setMenuImage(mi, Constants.ICON_DATAEXPLORER_SMALL);
2000        mi.addActionListener(this);
2001        mi.setActionCommand(ACT_SHOW_DASHBOARD);
2002        windowMenu.add(mi);
2003
2004        makeTabNavigationMenu(windowMenu);
2005
2006        @SuppressWarnings("unchecked") // it's how the IDV does it.
2007        List windows = new ArrayList(IdvWindow.getWindows());
2008        for (int i = 0; i < windows.size(); i++) {
2009            final IdvWindow window = (IdvWindow)windows.get(i);
2010
2011            // Skip the main window
2012            if (window.getIsAMainWindow()) {
2013                continue;
2014            }
2015
2016            String title = window.getTitle();
2017            String titleParts[] = splitTitle(title);
2018
2019            if (titleParts.length == 2) {
2020                title = titleParts[1];
2021            }
2022
2023            // Skip the data explorer and display controller
2024            String dataSelectorNameParts[] = splitTitle(Constants.DATASELECTOR_NAME);
2025            if (title.equals(Constants.DATASELECTOR_NAME) || title.equals(dataSelectorNameParts[1])) {
2026                continue;
2027            }
2028
2029            // Add a meaningful name if there is none
2030            if (title.isEmpty()) {
2031                title = "<Unnamed>";
2032            }
2033
2034            if (window.isVisible()) {
2035                mi = new JMenuItem(title);
2036                mi.addActionListener(new ActionListener() {
2037                    public void actionPerformed(ActionEvent ae) {
2038                        window.toFront();
2039                    }
2040                });
2041
2042                if (first) {
2043                    windowMenu.addSeparator();
2044                    first = false;
2045                }
2046
2047                windowMenu.add(mi);
2048            }
2049        }
2050        Msg.translateTree(windowMenu);
2051    }
2052
2053    /**
2054     * 
2055     * @param menu
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(new ActionListener() {
2352
2353                        public void actionPerformed(ActionEvent ae) {
2354                            if (!inWindow) {
2355                                createNewTab(skinid);
2356                            } else {
2357                                createNewWindow(null, true,
2358                                    getStateManager().getTitle(), skins.get(
2359                                        skinIndex).toString(), skins.getRoot(
2360                                        skinIndex, false), inWindow, null);
2361                            }
2362                        }
2363                    });
2364                    theMenu.add(mi);
2365                }
2366            }
2367
2368            // attach the dynamic skin menu item to the tab menu.
2369//            if (!inWindow) {
2370//                ((JMenu)newDisplayMenu).addSeparator();
2371//                IdvWindow window = IdvWindow.getActiveWindow();
2372//
2373//                final McvComponentGroup group =
2374//                    (McvComponentGroup)window.getComponentGroups().get(0);
2375//
2376//                JMenuItem mi = new JMenuItem("Choose Your Own Adventure...");
2377//                mi.addActionListener(new ActionListener() {
2378//
2379//                    public void actionPerformed(ActionEvent e) {
2380//                        makeDynamicSkin(group);
2381//                    }
2382//                });
2383//                newDisplayMenu.add(mi);
2384//            }
2385        }
2386        return newDisplayMenu;
2387    }
2388
2389    // for the time being just create some basic viewmanagers.
2390//    public void makeDynamicSkin(McvComponentGroup group) {
2391//        // 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...)
2392//        try {
2393//            Document doc = XmlUtil.getDocument(SKIN_TEMPLATE);
2394//            Element root = doc.getDocumentElement();
2395//            Element rightChild = doc.createElement("idv.view");
2396//            rightChild.setAttribute("class", "ucar.unidata.idv.TransectViewManager");
2397//            rightChild.setAttribute("viewid", "viewright1337");
2398//            rightChild.setAttribute("id", "viewright");
2399//            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%;");
2400//
2401//            Element leftChild = doc.createElement("idv.view");
2402//            leftChild.setAttribute("class", "ucar.unidata.idv.MapViewManager");
2403//            leftChild.setAttribute("viewid", "viewleft1337");
2404//            leftChild.setAttribute("id", "viewleft");
2405//            leftChild.setAttribute("properties", "name=Panel 2;clickToFocus=true;showToolBars=true;shareViews=true;showControlLegend=false;size=300:400;shareGroup=view%versionuid%;");
2406//
2407//            Element startNode = XmlUtil.findElement(root, "splitpane", "embeddednode", "true");
2408//            startNode.appendChild(rightChild);
2409//            startNode.appendChild(leftChild);
2410//            group.makeDynamicSkin(root);
2411//        } catch (Exception e) {
2412//            LogUtil.logException("Error: parsing skin template:", e);
2413//        }
2414//    }
2415//
2416//    private static final String SKIN_TEMPLATE = 
2417//        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
2418//        "<skin embedded=\"true\">\n" +
2419//        "  <ui>\n" +
2420//        "    <panel layout=\"border\" bgcolor=\"red\">\n" +
2421//        "      <idv.menubar place=\"North\"/>\n" +
2422//        "      <panel layout=\"border\" place=\"Center\">\n" +
2423//        "        <panel layout=\"flow\" place=\"North\">\n" +
2424//        "          <idv.toolbar id=\"idv.toolbar\" place=\"West\"/>\n" +
2425//        "          <panel id=\"idv.favoritesbar\" place=\"North\"/>\n" +
2426//        "        </panel>\n" +
2427//        "        <splitpane embeddednode=\"true\" resizeweight=\"0.5\" onetouchexpandable=\"true\" orientation=\"h\" bgcolor=\"blue\" layout=\"grid\" cols=\"2\" place=\"Center\">\n" +
2428//        "        </splitpane>\n" +
2429//        "      </panel>\n" +
2430//        "      <component idref=\"bottom_bar\"/>\n" +
2431//        "    </panel>\n" +
2432//        "  </ui>\n" +
2433//        "  <styles>\n" +
2434//        "    <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" +
2435//        "    <style class=\"textbtn\" space=\"2\" mouse_enter=\"ui.setText(idv.messagelabel,prop:tooltip)\" mouse_exit=\"ui.setText(idv.messagelabel,)\"/>\n" +
2436//        "  </styles>\n" +
2437//        "  <components>\n" +
2438//        "    <idv.statusbar place=\"South\" id=\"bottom_bar\"/>\n" +
2439//        "  </components>\n" +
2440//        "  <properties>\n" +
2441//        "    <property name=\"icon.wait.wait\" value=\"/ucar/unidata/idv/images/wait.gif\"/>\n" +
2442//        "  </properties>\n" +
2443//        "</skin>\n";
2444
2445    private int holderCount;
2446    
2447    /**
2448     * Associates a given ViewManager with a given ComponentHolder.
2449     * 
2450     * @param vm The ViewManager that is inside {@code holder}.
2451     * @param holder The ComponentHolder that contains {@code vm}.
2452     */
2453    public void setViewManagerHolder(ViewManager vm, ComponentHolder holder) {
2454        viewManagers.put(vm, holder);
2455        holderCount = getComponentHolders().size();
2456    }
2457
2458    public Set<ComponentHolder> getComponentHolders() {
2459        return newHashSet(viewManagers.values());
2460    }
2461
2462    public int getComponentHolderCount() {
2463        return holderCount;
2464    }
2465
2466    public int getComponentGroupCount() {
2467        return getComponentGroups().size();
2468    }
2469
2470    /**
2471     * Returns the ComponentHolder containing the given ViewManager.
2472     * 
2473     * @param vm The ViewManager whose ComponentHolder is needed.
2474     * 
2475     * @return Either null or the ComponentHolder.
2476     */
2477    public ComponentHolder getViewManagerHolder(ViewManager vm) {
2478        return viewManagers.get(vm);
2479    }
2480
2481    /**
2482     * Disassociate a given ViewManager from its ComponentHolder.
2483     * 
2484     * @return The associated 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     * @see ucar.unidata.idv.ui.IdvUIManager#showDashboard()
2496     */
2497    @Override
2498    public void showDashboard() {
2499        showDashboard("");
2500    }
2501
2502    /**
2503     * Creates the McVViewPanel component that shows up in the dashboard.
2504     */
2505    @Override
2506    protected ViewPanel doMakeViewPanel() {
2507        ViewPanel vp = new McIDASVViewPanel(idv);
2508        vp.getContents();
2509        return vp;
2510    }
2511
2512    /**
2513     * @return A map of skin ids to their index within the skin resource.
2514     */
2515    private Map<String, Integer> readSkinIds() {
2516        XmlResourceCollection skins = getResourceManager().getXmlResources(IdvResourceManager.RSC_SKIN);
2517        Map<String, Integer> ids = new HashMap<>(skins.size());
2518        for (int i = 0; i < skins.size(); i++) {
2519            String id = skins.getProperty("skinid", i);
2520            if (id != null) {
2521                ids.put(id, i);
2522            }
2523        }
2524        return ids;
2525    }
2526
2527    /**
2528     * Adds a skinned component holder to the active component group.
2529     * 
2530     * @param skinId The value of the skin's skinid attribute.
2531     */
2532    public void createNewTab(final String skinId) {
2533        IdvWindow activeWindow = IdvWindow.getActiveWindow();
2534        IdvComponentGroup group =
2535            McVGuiUtils.getComponentGroup(activeWindow);
2536        if (skinIds.containsKey(skinId)) {
2537            group.makeSkin(skinIds.get(skinId));
2538        }
2539        JFrame frame = activeWindow.getFrame();
2540        if (frame != null) {
2541            frame.setPreferredSize(frame.getSize());
2542        }
2543    }
2544
2545    /**
2546     * Method to do the work of showing the Data Explorer (nee Dashboard)
2547     */
2548    @SuppressWarnings("unchecked") // IdvWindow.getWindows only adds IdvWindows.
2549    public void showDashboard(String tabName) {
2550        if (!initDone) {
2551            return;
2552        } else if (dashboard == null) {
2553            showWaitCursor();
2554            doMakeBasicWindows();
2555            showNormalCursor();
2556            String title = makeTitle(getStateManager().getTitle(), Constants.DATASELECTOR_NAME);
2557            for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) {
2558                if (title.equals(window.getTitle())) {
2559                    dashboard = window;
2560                    dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
2561                }
2562            }
2563        } else {
2564            dashboard.show();
2565        }
2566
2567        if (tabName.isEmpty()) {
2568            return;
2569        }
2570
2571        // Dig two panels deep looking for a JTabbedPane
2572        // If you find one, try to show the requested tab name
2573        JComponent contents = dashboard.getContents();
2574        JComponent component = (JComponent)contents.getComponent(0);
2575        JTabbedPane tPane = null;
2576        if (component instanceof JTabbedPane) {
2577            tPane = (JTabbedPane)component;
2578        }
2579        else {
2580            JComponent component2 = (JComponent)component.getComponent(0);
2581            if (component2 instanceof JTabbedPane) {
2582                tPane = (JTabbedPane)component2;
2583            }
2584        }
2585        if (tPane != null) {
2586            for (int i=0; i<tPane.getTabCount(); i++) {
2587                if (tabName.equals(tPane.getTitleAt(i))) {
2588                    tPane.setSelectedIndex(i);
2589                    break;
2590                }
2591            }
2592        }
2593    }
2594
2595    /**
2596     * Show the support request form
2597     *
2598     * @param description Default value for the description form entry
2599     * @param stackTrace The stack trace that caused this error.
2600     * @param dialog The dialog to put the gui in, if non-null.
2601     */
2602    public void showSupportForm(final String description, 
2603        final String stackTrace, final JDialog dialog) 
2604    {
2605        java.awt.EventQueue.invokeLater(new Runnable() {
2606            public void run() {
2607                // TODO: mcvstatecollector should have a way to gather the
2608                // exception information..
2609                McIDASV mcv = (McIDASV)getIdv();
2610                new SupportForm(getStore(), new McvStateCollector(mcv)).setVisible(true);
2611            }
2612        });
2613    }
2614
2615    /**
2616     * Attempts to locate and display a dashboard component using an ID.
2617     * 
2618     * @param id ID of the desired component.
2619     * 
2620     * @return True if {@code id} corresponds to a component. False otherwise.
2621     */
2622    public boolean showDashboardComponent(String id) {
2623        Object comp = findComponent(id);
2624        if (comp != null) {
2625            GuiUtils.showComponentInTabs((JComponent)comp);
2626            return true;
2627        } else {
2628            super.showDashboard();
2629            for (IdvWindow window : (List<IdvWindow>)IdvWindow.getWindows()) {
2630                String title = makeTitle(
2631                    getStateManager().getTitle(),
2632                    Constants.DATASELECTOR_NAME
2633                );
2634                if (title.equals(window.getTitle())) {
2635                    dashboard = window;
2636                    dashboard.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
2637                }
2638            }
2639        }
2640        return false;
2641    }
2642
2643    /**
2644     * Close and dispose of the splash window (if it has been created).
2645     */
2646    @Override
2647    public void splashClose() {
2648        if (splash != null) {
2649            splash.doClose();
2650        }
2651    }
2652
2653    /**
2654     * Show a message in the splash screen (if it exists)
2655     *
2656     * @param m The message to show
2657     */
2658    @Override public void splashMsg(String m) {
2659        if (splash != null) {
2660            splash.splashMsg(m);
2661        }
2662    }
2663
2664    /**
2665     * Uses a given toolbar editor to repopulate all toolbars so that they 
2666     * correspond to the user's choice of actions.
2667     * 
2668     * @param tbe The toolbar editor that contains the actions the user wants.
2669     */
2670    public void setCurrentToolbars(final McvToolbarEditor tbe) {
2671        List<TwoFacedObject> tfos = tbe.getTLP().getCurrentEntries();
2672        List<String> buttonIds = new ArrayList<>(tfos.size());
2673        for (TwoFacedObject tfo : tfos) {
2674            if (McvToolbarEditor.isSpace(tfo)) {
2675                buttonIds.add((String)null);
2676            } else {
2677                buttonIds.add(TwoFacedObject.getIdString(tfo));
2678            }
2679        }
2680
2681        cachedButtons = buttonIds;
2682
2683        for (JToolBar toolbar : toolbars) {
2684            toolbar.setVisible(false);
2685            populateToolbar(toolbar);
2686            toolbar.setVisible(true);
2687        }
2688    }
2689
2690    /**
2691     * Append a string and object to the buffer
2692     *
2693     * @param sb  StringBuffer to append to
2694     * @param name  Name of the object
2695     * @param value  the object value
2696     */
2697    private void append(StringBuffer sb, String name, Object value) {
2698        sb.append("<b>").append(name).append("</b>: ").append(value).append("<br>");
2699    }
2700
2701    private JMenuItem makeControlDescriptorItem(ControlDescriptor cd) {
2702        JMenuItem mi = new JMenuItem();
2703        if (cd != null) {
2704            mi = new JMenuItem(cd.getLabel());
2705            mi.addActionListener(new ObjectListener(cd) {
2706                public void actionPerformed(ActionEvent ev) {
2707                    idv.doMakeControl(new ArrayList(),
2708                        (ControlDescriptor)theObject);
2709                }
2710            });
2711        }
2712        return mi;
2713    }
2714
2715    /* (non-javadoc)
2716     * Overridden so that the toolbar will update upon saving a bundle.
2717     */
2718    @Override public void displayTemplatesChanged() {
2719        super.displayTemplatesChanged();
2720        for (JToolBar toolbar : toolbars) {
2721            toolbar.setVisible(false);
2722            populateToolbar(toolbar);
2723            toolbar.setVisible(true);
2724        }
2725    }
2726
2727    /**
2728     * Called when there has been any change to the favorite bundles and is
2729     * most useful for triggering an update to the {@literal "toolbar bundles"}.
2730     */
2731    @Override public void favoriteBundlesChanged() {
2732        SwingUtilities.invokeLater(new Runnable() {
2733            public void run() {
2734                for (JToolBar toolbar : toolbars) {
2735                    toolbar.setVisible(false);
2736                    populateToolbar(toolbar);
2737                    toolbar.setVisible(true);
2738                }
2739            }
2740        });
2741    }
2742
2743    /**
2744     * Show the support request form in a non-swing thread. We do this because we cannot
2745     * call the HttpFormEntry.showUI from a swing thread
2746     *
2747     * @param description Default value for the description form entry
2748     * @param stackTrace The stack trace that caused this error.
2749     * @param dialog The dialog to put the gui in, if non-null.
2750     */
2751
2752    private void showSupportFormInThread(String description,
2753                                         String stackTrace, JDialog dialog) {
2754        List<HttpFormEntry> entries = new ArrayList<>();
2755
2756        StringBuffer extra   = new StringBuffer("<h3>McIDAS-V</h3>\n");
2757        Hashtable<String, String> table = 
2758            ((StateManager)getStateManager()).getVersionInfo();
2759        append(extra, "mcv.version.general", table.get("mcv.version.general"));
2760        append(extra, "mcv.version.build", table.get("mcv.version.build"));
2761        append(extra, "idv.version.general", table.get("idv.version.general"));
2762        append(extra, "idv.version.build", table.get("idv.version.build"));
2763
2764        extra.append("<h3>OS</h3>\n");
2765        append(extra, "os.name", System.getProperty("os.name"));
2766        append(extra, "os.arch", System.getProperty("os.arch"));
2767        append(extra, "os.version", System.getProperty("os.version"));
2768
2769        extra.append("<h3>Java</h3>\n");
2770        append(extra, "java.vendor", System.getProperty("java.vendor"));
2771        append(extra, "java.version", System.getProperty("java.version"));
2772        append(extra, "java.home", System.getProperty("java.home"));
2773
2774        StringBuffer javaInfo = new StringBuffer();
2775        javaInfo.append("Java: home: " + System.getProperty("java.home"));
2776        javaInfo.append(" version: " + System.getProperty("java.version"));
2777
2778        Class c = null;
2779        try {
2780            c = Class.forName("javax.media.j3d.VirtualUniverse");
2781            Method method = Misc.findMethod(c, "getProperties",
2782                                            new Class[] {});
2783            if (method == null) {
2784                javaInfo.append("j3d <1.3");
2785            } else {
2786                try {
2787                    Map m = (Map)method.invoke(c, new Object[] {});
2788                    javaInfo.append(" j3d:" + m.get("j3d.version"));
2789                    append(extra, "j3d.version", m.get("j3d.version"));
2790                    append(extra, "j3d.vendor", m.get("j3d.vendor"));
2791                    append(extra, "j3d.renderer", m.get("j3d.renderer"));
2792                } catch (Exception exc) {
2793                    javaInfo.append(" j3d:" + "unknown");
2794                }
2795            }
2796        } catch (ClassNotFoundException exc) {
2797            append(extra, "j3d", "none");
2798        }
2799
2800        boolean persistCC = getStore().get("mcv.supportreq.cc", true);
2801
2802        JCheckBox ccMyself = new JCheckBox("Send Copy of Support Request to Me", persistCC);
2803        ccMyself.addActionListener(new ActionListener() {
2804            public void actionPerformed(final ActionEvent e) {
2805                JCheckBox cb = (JCheckBox)e.getSource();
2806                getStore().put("mcv.supportreq.cc", cb.isSelected());
2807            }
2808        });
2809
2810        boolean doWrap = idv.getProperty(PROP_WRAP_SUPPORT_DESC, true);
2811
2812        HttpFormEntry descriptionEntry;
2813        HttpFormEntry nameEntry;
2814        HttpFormEntry emailEntry;
2815        HttpFormEntry orgEntry;
2816
2817        entries.add(nameEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2818                "form_data[fromName]", "Name:",
2819                getStore().get(PROP_HELP_NAME, (String) null)));
2820        entries.add(emailEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2821                "form_data[email]", "Your Email:",
2822                getStore().get(PROP_HELP_EMAIL, (String) null)));
2823        entries.add(orgEntry = new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2824                "form_data[organization]", "Organization:",
2825                getStore().get(PROP_HELP_ORG, (String) null)));
2826        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_INPUT,
2827                                      "form_data[subject]", "Subject:"));
2828
2829        entries.add(
2830            new HttpFormEntry(
2831                HttpFormEntry.TYPE_LABEL, "",
2832                "<html>Please provide a <i>thorough</i> description of the problem you encountered:</html>"));
2833        entries.add(descriptionEntry =
2834            new FormEntry(doWrap, HttpFormEntry.TYPE_AREA,
2835                              "form_data[description]", "Description:",
2836                              description, 5, 30, true));
2837
2838        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE,
2839                                      "form_data[att_two]", "Attachment 1:", "",
2840                                      false));
2841        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_FILE,
2842                                      "form_data[att_three]", "Attachment 2:", "",
2843                                      false));
2844
2845        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2846                                      "form_data[submit]", "", "Send Email"));
2847        
2848        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2849                                      "form_data[p_version]", "",
2850                                      getStateManager().getVersion()
2851                                      + " build date:"
2852                                      + getStateManager().getBuildDate()));
2853        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2854                                      "form_data[opsys]", "",
2855                                      System.getProperty("os.name")));
2856        entries.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN,
2857                                      "form_data[hardware]", "",
2858                                      javaInfo.toString()));
2859        
2860        JLabel topLabel = 
2861            new JLabel("<html>This form allows you to send a support request to the McIDAS Help Desk.<br></html>");
2862
2863        JCheckBox includeBundleCbx =
2864            new JCheckBox("Include Current State as Bundle", false);
2865
2866        List<JCheckBox> checkboxes = list(includeBundleCbx, ccMyself);
2867
2868        boolean alreadyHaveDialog = true;
2869        if (dialog == null) {
2870            // NOTE: if the dialog is modeless you can leave alreadyHaveDialog
2871            // alone. If the dialog is modal you need to set alreadyHaveDialog
2872            // to false.
2873            // If alreadyHaveDialog is false with a modeless dialog, the later
2874            // call to HttpFormEntry.showUI will return false and break out of
2875            // the while loop without talking to the HTTP server.
2876            dialog = GuiUtils.createDialog(LogUtil.getCurrentWindow(),
2877                                           "Support Request Form", false);
2878//            alreadyHaveDialog = false;
2879        }
2880
2881        JLabel statusLabel = GuiUtils.cLabel(" ");
2882        JComponent bottom = LayoutUtil.vbox(LayoutUtil.leftVbox(checkboxes), statusLabel);
2883
2884        while (true) {
2885            //Show form. Check if user pressed cancel.
2886            statusLabel.setText(" ");
2887            if ( !HttpFormEntry.showUI(entries, LayoutUtil.inset(topLabel, 10),
2888                                       bottom, dialog, alreadyHaveDialog)) {
2889                break;
2890            }
2891            statusLabel.setText("Posting support request...");
2892
2893            //Save persistent state
2894            getStore().put(PROP_HELP_NAME, nameEntry.getValue());
2895            getStore().put(PROP_HELP_ORG, orgEntry.getValue());
2896            getStore().put(PROP_HELP_EMAIL, emailEntry.getValue());
2897            getStore().save();
2898
2899            List<HttpFormEntry> entriesToPost = 
2900                new ArrayList<>(entries);
2901
2902            if ((stackTrace != null) && (stackTrace.length() > 0)) {
2903                entriesToPost.remove(descriptionEntry);
2904                String newDescription =
2905                    descriptionEntry.getValue()
2906                    + "\n\n******************\nStack trace:\n" + stackTrace;
2907                entriesToPost.add(
2908                    new HttpFormEntry(
2909                        HttpFormEntry.TYPE_HIDDEN, "form_data[description]",
2910                        "Description:", newDescription, 5, 30, true));
2911            }
2912
2913            try {
2914                extra.append(idv.getPluginManager().getPluginHtml());
2915                extra.append(getResourceManager().getHtmlView());
2916
2917                entriesToPost.add(new HttpFormEntry("form_data[att_extra]",
2918                    "extra.html", extra.toString().getBytes()));
2919
2920                if (includeBundleCbx.isSelected()) {
2921                    entriesToPost.add(
2922                        new HttpFormEntry(
2923                            "form_data[att_state]", "bundle" + Constants.SUFFIX_MCV,
2924                            idv.getPersistenceManager().getBundleXml(
2925                                true).getBytes()));
2926                }
2927                entriesToPost.add(new HttpFormEntry(HttpFormEntry.TYPE_HIDDEN, 
2928                    "form_data[cc_user]", "", 
2929                    Boolean.toString(getStore().get("mcv.supportreq.cc", true))));
2930
2931                String[] results = 
2932                    HttpFormEntry.doPost(entriesToPost, SUPPORT_REQ_URL);
2933
2934                if (results[0] != null) {
2935                    GuiUtils.showHtmlDialog(
2936                        results[0], "Support Request Response - Error",
2937                        "Support Request Response - Error", null, true);
2938                    continue;
2939                }
2940                String html = results[1];
2941                if (html.toLowerCase().indexOf("your email has been sent")
2942                        >= 0) {
2943                    LogUtil.userMessage("Your support request has been sent");
2944                    break;
2945                } else if (html.toLowerCase().indexOf("required fields")
2946                           >= 0) {
2947                    LogUtil.userErrorMessage(
2948                        "<html>There was a problem submitting your request. <br>Is your email correct?</html>");
2949                } else {
2950                    GuiUtils.showHtmlDialog(
2951                        html, "Unknown Support Request Response",
2952                        "Unknown Support Request Response", null, true);
2953                    System.err.println(html.toLowerCase());
2954                }
2955            } catch (Exception exc) {
2956                LogUtil.logException("Doing support request form", exc);
2957            }
2958        }
2959        dialog.dispose();
2960    }
2961
2962    @Override protected IdvXmlUi doMakeIdvXmlUi(IdvWindow window, 
2963        List viewManagers, Element skinRoot) 
2964    {
2965        return new McIDASVXmlUi(window, viewManagers, idv, skinRoot);
2966    }
2967
2968    /**
2969     * DeInitialize the given menu before it is shown
2970     * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu)
2971     */
2972    @Override
2973    protected void handleMenuDeSelected(final String id, final JMenu menu, final IdvWindow idvWindow) {
2974        super.handleMenuDeSelected(id, menu, idvWindow);
2975    }
2976
2977    /**
2978     * Initialize the given menu before it is shown
2979     * @see ucar.unidata.idv.ui.IdvUIManager#historyMenuSelected(JMenu)
2980     */
2981    @Override
2982    protected void handleMenuSelected(final String id, final JMenu menu, final IdvWindow idvWindow) {
2983        if (id.equals(MENU_NEWVIEWS)) {
2984            ViewManager last = getVMManager().getLastActiveViewManager();
2985            menu.removeAll();
2986            makeViewStateMenu(menu, last);
2987        } else if (id.equals("bundles")) {
2988            menu.removeAll();
2989            makeBundleMenu(menu);
2990        } else if (id.equals(MENU_NEWDISPLAY_TAB)) {
2991            menu.removeAll();
2992            doMakeNewDisplayMenu(menu, false);
2993        } else if (id.equals(MENU_NEWDISPLAY)) {
2994            menu.removeAll();
2995            doMakeNewDisplayMenu(menu, true);
2996        } else if (id.equals("menu.tools.projections.deletesaved")) {
2997            menu.removeAll();
2998            makeDeleteViewsMenu(menu);
2999        } else if (id.equals("file.default.layout")) {
3000            makeDefaultLayoutMenu(menu);
3001        } else if (id.equals("tools.formulas")) {
3002            menu.removeAll();
3003            makeFormulasMenu(menu);
3004        } else {
3005            super.handleMenuSelected(id, menu, idvWindow);
3006        }
3007    }
3008
3009    /** A cache of the operand name to value for the user choices */
3010    private Map operandCache;
3011
3012    @Override public List selectUserChoices(String msg, List userOperands) {
3013        if (operandCache == null) {
3014            operandCache =
3015                (Hashtable) getStore().getEncodedFile("operandcache.xml");
3016            if (operandCache == null) {
3017                operandCache = new Hashtable();
3018            }
3019        }
3020        List fields         = new ArrayList();
3021        List components     = new ArrayList();
3022        List persistentCbxs = new ArrayList();
3023        components.add(new JLabel("Property"));
3024        components.add(new JLabel("Value"));
3025        components.add(new JLabel("Save in Bundle"));
3026        for (int i = 0; i < userOperands.size(); i++) {
3027            DataOperand operand   = (DataOperand) userOperands.get(i);
3028            String      fieldType = (String) operand.getProperty("type");
3029            if (fieldType == null) {
3030                fieldType = FIELDTYPE_TEXT;
3031            }
3032            DerivedDataChoice formula = operand.getDataChoice();
3033            String description = operand.getDescription();
3034            if (formula != null) {
3035                description = formula.toString();
3036            }
3037
3038            String label = operand.getLabel();
3039            Object dflt = operand.getUserDefault();
3040            Object cacheKeyNewStyle = Misc.newList(description, label, fieldType);
3041            Object cacheKey = Misc.newList(label, fieldType);
3042
3043            Object cachedOperand = null;
3044            boolean oldStyle = operandCache.containsKey(cacheKey);
3045            boolean newStyle = operandCache.containsKey(cacheKeyNewStyle);
3046
3047            // if new style, always use that and ignore old style
3048            // if no new style, proceed as before.
3049            if (newStyle) {
3050                cachedOperand = operandCache.get(cacheKeyNewStyle);
3051            } else if (oldStyle) {
3052                cachedOperand = operandCache.get(cacheKey);
3053            }
3054
3055            if (cachedOperand != null) {
3056                dflt = cachedOperand;
3057            }
3058
3059            JCheckBox cbx = new JCheckBox("", operand.isPersistent());
3060            persistentCbxs.add(cbx);
3061            JComponent field     = null;
3062            JComponent fieldComp = null;
3063            if (fieldType.equals(FIELDTYPE_TEXT)) {
3064                String rowString = (String) operand.getProperty("rows");
3065                if (rowString == null) {
3066                    rowString = "1";
3067                }
3068                int rows = new Integer(rowString).intValue();
3069                if (rows == 1) {
3070                    field = new JTextField((dflt != null)
3071                        ? dflt.toString()
3072                        : "", 15);
3073                } else {
3074                    field     = new JTextArea((dflt != null)
3075                        ? dflt.toString()
3076                        : "", rows, 15);
3077                    fieldComp = GuiUtils.makeScrollPane(field, 200, 100);
3078                }
3079            } else if (fieldType.equals(FIELDTYPE_BOOLEAN)) {
3080                field = new JCheckBox("", ((dflt != null)
3081                    ? new Boolean(
3082                    dflt.toString()).booleanValue()
3083                    : true));
3084            } else if (fieldType.equals(FIELDTYPE_CHOICE)) {
3085                String choices = (String) operand.getProperty("choices");
3086                if (choices == null) {
3087                    throw new IllegalArgumentException(
3088                        "No 'choices' attribute defined for operand: "
3089                            + operand);
3090                }
3091                List l = StringUtil.split(choices, ";", true, true);
3092                field = new JComboBox(new Vector(l));
3093                if ((dflt != null) && l.contains(dflt)) {
3094                    ((JComboBox) field).setSelectedItem(dflt);
3095                }
3096            } else if (fieldType.equals(FIELDTYPE_FILE)) {
3097                JTextField fileFld = new JTextField(((dflt != null)
3098                    ? dflt.toString()
3099                    : ""), 30);
3100                field = fileFld;
3101                String patterns = operand.getProperty("filepattern");
3102                List   filters  = null;
3103                if (patterns != null) {
3104                    filters = new ArrayList();
3105                    List toks = StringUtil.split(patterns, ";", true, true);
3106                    for (int tokIdx = 0; tokIdx < toks.size(); tokIdx++) {
3107                        String tok   = (String) toks.get(tokIdx);
3108                        List subToks = StringUtil.split(tok, ":", true, true);
3109                        if (subToks.size() == 2) {
3110                            filters.add(
3111                                new PatternFileFilter(
3112                                    (String) subToks.get(0),
3113                                    (String) subToks.get(1)));
3114                        } else {
3115                            filters.add(new PatternFileFilter(tok, tok));
3116                        }
3117                    }
3118                }
3119                fieldComp = GuiUtils.centerRight(GuiUtils.hfill(fileFld),
3120                    GuiUtils.makeFileBrowseButton(fileFld, filters));
3121            } else if (fieldType.equals(FIELDTYPE_LOCATION)) {
3122                List l = ((dflt != null)
3123                    ? StringUtil.split(dflt.toString(), ";", true, true)
3124                    : (List) new ArrayList());
3125                final LatLonWidget llw = new LatLonWidget();
3126                field = llw;
3127                if (l.size() == 2) {
3128                    llw.setLat(Misc.decodeLatLon(l.get(0).toString()));
3129                    llw.setLon(Misc.decodeLatLon(l.get(1).toString()));
3130                }
3131                final JButton centerPopupBtn =
3132                    GuiUtils.getImageButton("/auxdata/ui/icons/Map16.gif",
3133                        getClass());
3134                centerPopupBtn.setToolTipText("Center on current displays");
3135                centerPopupBtn.addActionListener(new ActionListener() {
3136                    public void actionPerformed(ActionEvent ae) {
3137                        popupCenterMenu(centerPopupBtn, llw);
3138                    }
3139                });
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 = (String) 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(new ActionListener() {
3226            public void actionPerformed(final ActionEvent e) {
3227                ((McIDASV)idv).doSaveAsDefaultLayout();
3228            }
3229        });
3230
3231        JMenuItem removeLayout = new JMenuItem("Remove");
3232        McVGuiUtils.setMenuImage(removeLayout, Constants.ICON_DEFAULTLAYOUTDELETE_SMALL);
3233        removeLayout.setToolTipText("Remove saved default layout");
3234        removeLayout.addActionListener(new ActionListener() {
3235            public void actionPerformed(final ActionEvent e) {
3236                idv.doClearDefaults();
3237            }
3238        });
3239
3240        removeLayout.setEnabled(((McIDASV)idv).hasDefaultLayout());
3241
3242        menu.add(saveLayout);
3243        menu.add(removeLayout);
3244    }
3245
3246    /**
3247     * Bundles any compatible {@link ViewManager} states into {@link JMenuItem}s
3248     * and adds said {@code JMenuItem}s to {@code menu}. Incompatible states are
3249     * ignored.
3250     * 
3251     * <p>Each {@code JMenuItem} (except those under the {@literal "Delete"} menu--apologies)
3252     * associates a {@literal "view state"} and an {@link ObjectListener}. 
3253     * The {@code ObjectListener} uses this associated view state to attempt reinitialization
3254     * of {@code vm}.
3255     * 
3256     * <p>Override reasoning:
3257     * <ul>
3258     *   <li>terminology ({@literal "views"} rather than {@literal "viewpoints"}).</li>
3259     *   <li>
3260     *     use of {@link #filterVMMStatesWithVM(ViewManager, Collection)} to
3261     *     properly detect the {@literal "no saved views"} case.
3262     *   </li>
3263     * </ul>
3264     * 
3265     * @param menu Menu to populate. Should not be {@code null}.
3266     * @param vm {@code ViewManager} that might get reinitialized. Should not be {@code null}. 
3267     * 
3268     * @see ViewManager#initWith(ViewManager, boolean)
3269     * @see ViewManager#initWith(ViewState)
3270     * @see IdvUIManager#makeViewStateMenu(JMenu, ViewManager)
3271     */
3272    @Override public void makeViewStateMenu(final JMenu menu, final ViewManager vm) {
3273        List<TwoFacedObject> vmStates = filterVMMStatesWithVM(vm, getVMManager().getVMState());
3274        if (vmStates.isEmpty()) {
3275            JMenuItem item = new JMenuItem(Msg.msg("No Saved Views"));
3276            item.setEnabled(false);
3277            menu.add(item);
3278        } else {
3279            JMenu deleteMenu = new JMenu("Delete");
3280            makeDeleteViewsMenu(deleteMenu);
3281            menu.add(deleteMenu);
3282        }
3283
3284        for (TwoFacedObject tfo : vmStates) {
3285            JMenuItem mi = new JMenuItem(tfo.getLabel().toString());
3286            menu.add(mi);
3287            mi.addActionListener(new ObjectListener(tfo.getId()) {
3288                public void actionPerformed(final ActionEvent e) {
3289                    if (vm == null) {
3290                        return;
3291                    }
3292
3293                    if (theObject instanceof ViewManager) {
3294                        vm.initWith((ViewManager)theObject, true);
3295                    } else if (theObject instanceof ViewState) {
3296                        try {
3297                            vm.initWith((ViewState)theObject);
3298                        } catch (Throwable ex) {
3299                            logException("Initializing view with ViewState", ex);
3300                        }
3301                    } else {
3302                        LogUtil.consoleMessage("UIManager.makeViewStateMenu: Object of unknown type: "+theObject.getClass().getName());
3303                    }
3304                }
3305            });
3306        }
3307    }
3308
3309    /**
3310     * Returns a list of {@link TwoFacedObject}s that are known to be 
3311     * compatible with {@code vm}.
3312     * 
3313     * <p>This method is currently capable of dealing with {@code TwoFacedObject}s and
3314     * {@link ViewState}s within {@code states}. Any other types are ignored.
3315     * 
3316     * @param vm {@link ViewManager} to use for compatibility tests. {@code null} is allowed.
3317     * @param states Collection of objects to test against {@code vm}. {@code null} is allowed.
3318     * 
3319     * @return Either a {@link List} of compatible {@literal "view states"} or an empty {@code List}.
3320     * 
3321     * @see ViewManager#isCompatibleWith(ViewManager)
3322     * @see ViewManager#isCompatibleWith(ViewState)
3323     * @see #makeViewStateMenu(JMenu, ViewManager)
3324     */
3325    public static List<TwoFacedObject> filterVMMStatesWithVM(final ViewManager vm, final Collection<?> states) {
3326        if ((vm == null) || (states == null) || states.isEmpty()) {
3327            return Collections.emptyList();
3328        }
3329
3330        List<TwoFacedObject> validStates = new ArrayList<>(states.size());
3331        for (Object obj : states) {
3332            TwoFacedObject tfo = null;
3333            if (obj instanceof TwoFacedObject) {
3334                tfo = (TwoFacedObject)obj;
3335                if (vm.isCompatibleWith((ViewManager)tfo.getId())) {
3336                    continue;
3337                }
3338            } else if (obj instanceof ViewState) {
3339                if (!vm.isCompatibleWith((ViewState)obj)) {
3340                    continue;
3341                }
3342                tfo = new TwoFacedObject(((ViewState)obj).getName(), obj);
3343            } else {
3344                LogUtil.consoleMessage("UIManager.filterVMMStatesWithVM: Object of unknown type: "+obj.getClass().getName());
3345                continue;
3346            }
3347            validStates.add(tfo);
3348        }
3349        return validStates;
3350    }
3351
3352    /**
3353     * Overridden to build a custom Display menu.
3354     * @see ucar.unidata.idv.ui.IdvUIManager#initializeDisplayMenu(JMenu)
3355     */
3356    @Override protected void initializeDisplayMenu(JMenu displayMenu) {
3357        JMenu m;
3358        JMenuItem mi;
3359
3360        // Get the list of possible standalone control descriptors
3361        Hashtable controlsHash = new Hashtable();
3362        List controlDescriptors = getStandAloneControlDescriptors();
3363        for (int i = 0; i < controlDescriptors.size(); i++) {
3364            ControlDescriptor cd = (ControlDescriptor)controlDescriptors.get(i);
3365            String cdLabel = cd.getLabel();
3366            if (cdLabel.equals("Range Rings")) {
3367                controlsHash.put(cdLabel, cd);
3368            } else if (cdLabel.equals("Range and Bearing")) {
3369                controlsHash.put(cdLabel, cd);
3370            } else if (cdLabel.equals("Location Indicator")) {
3371                controlsHash.put(cdLabel, cd);
3372            } else if (cdLabel.equals("Drawing Control")) {
3373                controlsHash.put(cdLabel, cd);
3374            } else if (cdLabel.equals("Transect Drawing Control")) {
3375                controlsHash.put(cdLabel, cd);
3376            }
3377        }
3378
3379        // Build the menu
3380        ControlDescriptor cd;
3381
3382        mi = new JMenuItem("Create Layer from Data Source...");
3383        mi.addActionListener(new ActionListener() {
3384            @Override public void actionPerformed(ActionEvent ae) {
3385                showDashboard("Data Sources");
3386            }
3387        });
3388        displayMenu.add(mi);
3389
3390        mi = new JMenuItem("Layer Controls...");
3391        mi.addActionListener(new ActionListener() {
3392            @Override public void actionPerformed(ActionEvent ae) {
3393                showDashboard("Layer Controls");
3394            }
3395        });
3396        displayMenu.add(mi);
3397
3398        displayMenu.addSeparator();
3399
3400        cd = (ControlDescriptor)controlsHash.get("Range Rings");
3401        mi = makeControlDescriptorItem(cd);
3402        mi.setText("Add Range Rings");
3403        displayMenu.add(mi);
3404
3405        cd = (ControlDescriptor)controlsHash.get("Range and Bearing");
3406        mi = makeControlDescriptorItem(cd);
3407        McVGuiUtils.setMenuImage(mi, Constants.ICON_RANGEANDBEARING_SMALL);
3408        mi.setText("Add Range and Bearing");
3409        displayMenu.add(mi);
3410
3411        displayMenu.addSeparator();
3412
3413        cd = (ControlDescriptor)controlsHash.get("Transect Drawing Control");
3414        mi = makeControlDescriptorItem(cd);
3415        mi.setText("Draw Transect...");
3416        displayMenu.add(mi);
3417
3418        cd = (ControlDescriptor)controlsHash.get("Drawing Control");
3419        mi = makeControlDescriptorItem(cd);
3420        mi.setText("Draw Freely...");
3421        displayMenu.add(mi);
3422
3423        displayMenu.addSeparator();
3424
3425        cd = (ControlDescriptor)controlsHash.get("Location Indicator");
3426        mi = makeControlDescriptorItem(cd);
3427        McVGuiUtils.setMenuImage(mi, Constants.ICON_LOCATION_SMALL);
3428        mi.setText("Add Location Indicator");
3429        displayMenu.add(mi);
3430
3431        ControlDescriptor locationDescriptor = idv.getControlDescriptor("locationcontrol");
3432        if (locationDescriptor != null) {
3433            List stations = idv.getLocationList();
3434            ObjectListener listener = new ObjectListener(locationDescriptor) {
3435                public void actionPerformed(ActionEvent ae, Object obj) {
3436                    addStationDisplay((NamedStationTable) obj, (ControlDescriptor) theObject);
3437                }
3438            };
3439            List menuItems = NamedStationTable.makeMenuItems(stations, listener);
3440            displayMenu.add(MenuUtil.makeMenu("Plot Location Labels", menuItems));
3441        }
3442
3443        displayMenu.addSeparator();
3444
3445        mi = new JMenuItem("Add Background Image");
3446        McVGuiUtils.setMenuImage(mi, Constants.ICON_BACKGROUND_SMALL);
3447        mi.addActionListener(new ActionListener() {
3448            public void actionPerformed(ActionEvent ae) {
3449                getIdv().doMakeBackgroundImage();
3450            }
3451        });
3452        displayMenu.add(mi);
3453
3454        mi = new JMenuItem("Reset Map Layer to Defaults");
3455        mi.addActionListener(new ActionListener() {
3456            public void actionPerformed(ActionEvent ae) {
3457                // TODO: Call IdvUIManager.addDefaultMap()... should be made private
3458//                addDefaultMap();
3459                ControlDescriptor mapDescriptor = idv.getControlDescriptor("mapdisplay");
3460                if (mapDescriptor == null) {
3461                    return;
3462                }
3463                String attrs = "initializeAsDefault=true;displayName=Default Background Maps;";
3464                idv.doMakeControl(new ArrayList(), mapDescriptor, attrs, null);
3465            }
3466        });
3467        displayMenu.add(mi);
3468        Msg.translateTree(displayMenu);
3469    }
3470
3471    /**
3472     * Get the window title from the skin
3473     *
3474     * @param index  the skin index
3475     *
3476     * @return  the title
3477     */
3478    private String getWindowTitleFromSkin(final int index) {
3479        if (!skinToTitle.containsKey(index)) {
3480            IdvResourceManager mngr = getResourceManager();
3481            XmlResourceCollection skins = mngr.getXmlResources(mngr.RSC_SKIN);
3482            List<String> names = StringUtil.split(skins.getShortName(index), ">", true, true);
3483            String title = getStateManager().getTitle();
3484            if (!names.isEmpty()) {
3485                title = title + " - " + StringUtil.join(" - ", names);
3486            }
3487            skinToTitle.put(index, title);
3488        }
3489        return skinToTitle.get(index);
3490    }
3491
3492    @SuppressWarnings("unchecked")
3493    @Override public Hashtable getMenuIds() {
3494        return menuIds;
3495    }
3496
3497    @SuppressWarnings("unchecked")
3498    @Override public JMenuBar doMakeMenuBar(final IdvWindow idvWindow) {
3499        Hashtable<String, JMenuItem> menuMap = new Hashtable<>();
3500        JMenuBar menuBar = new JMenuBar();
3501        final IdvResourceManager mngr = getResourceManager();
3502        XmlResourceCollection xrc = mngr.getXmlResources(mngr.RSC_MENUBAR);
3503        Hashtable<String, ImageIcon> actionIcons = new Hashtable<>();
3504
3505        for (int i = 0; i < xrc.size(); i++) {
3506            GuiUtils.processXmlMenuBar(xrc.getRoot(i), menuBar, getIdv(), menuMap, actionIcons);
3507        }
3508
3509        menuIds = new Hashtable<>(menuMap);
3510
3511        // Ensure that the "help" menu is the last menu.
3512        JMenuItem helpMenu = menuMap.get(MENU_HELP);
3513        if (helpMenu != null) {
3514            menuBar.remove(helpMenu);
3515            menuBar.add(helpMenu);
3516        }
3517
3518        //TODO: Perhaps we will put the different skins in the menu?
3519        JMenu newDisplayMenu = (JMenu)menuMap.get(MENU_NEWDISPLAY);
3520        if (newDisplayMenu != null) {
3521            MenuUtil.makeMenu(newDisplayMenu, makeSkinMenuItems(makeMenuBarActionListener(), true, false));
3522        }
3523
3524//        final JMenu publishMenu = menuMap.get(MENU_PUBLISH);
3525//        if (publishMenu != null) {
3526//            if (!getPublishManager().isPublishingEnabled())
3527//                publishMenu.getParent().remove(publishMenu);
3528//            else
3529//                getPublishManager().initMenu(publishMenu);
3530//        }
3531
3532        for (Entry<String, JMenuItem> e : menuMap.entrySet()) {
3533            if (!(e.getValue() instanceof JMenu)) {
3534                continue;
3535            }
3536            String menuId = e.getKey();
3537            JMenu menu = (JMenu)e.getValue();
3538            menu.addMenuListener(makeMenuBarListener(menuId, menu, idvWindow));
3539        }
3540        return menuBar;
3541    }
3542
3543    private final ActionListener makeMenuBarActionListener() {
3544        final IdvResourceManager mngr = getResourceManager();
3545        return new ActionListener() {
3546            public void actionPerformed(final ActionEvent 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
3556    private final MenuListener makeMenuBarListener(final String id, final JMenu menu, final IdvWindow idvWindow) {
3557        return new MenuListener() {
3558            public void menuCanceled(final MenuEvent e) { }
3559            public void menuDeselected(final MenuEvent e) { handleMenuDeSelected(id, menu, idvWindow); }
3560            public void menuSelected(final MenuEvent e) { handleMenuSelected(id, menu, idvWindow); }
3561        };
3562    }
3563
3564    /**
3565     * Handle mouse clicks that occur within the toolbar.
3566     */
3567    private class PopupListener extends MouseAdapter {
3568
3569        private JPopupMenu popup;
3570
3571        public PopupListener(JPopupMenu p) {
3572            popup = p;
3573        }
3574
3575        // handle right clicks on os x and linux
3576        public void mousePressed(MouseEvent e) {
3577            if (e.isPopupTrigger()) {
3578                popup.show(e.getComponent(), e.getX(), e.getY());
3579            }
3580        }
3581
3582        // Windows doesn't seem to trigger mousePressed() for right clicks, but
3583        // never fear; mouseReleased() does the job.
3584        public void mouseReleased(MouseEvent e) {
3585            if (e.isPopupTrigger()) {
3586                popup.show(e.getComponent(), e.getX(), e.getY());
3587            }
3588        }
3589    }
3590
3591    /**
3592     * Handle (polymorphically) the {@link ucar.unidata.idv.ui.DataControlDialog}.
3593     * This dialog is used to either select a display control to create
3594     * or is used to set the timers used for a {@link ucar.unidata.data.DataSource}.
3595     *
3596     * @param dcd The dialog
3597     */
3598    public void processDialog(DataControlDialog dcd) {
3599        int estimatedMB = getEstimatedMegabytes(dcd);
3600        if (estimatedMB > 0) {
3601            double totalMem = Runtime.getRuntime().maxMemory();
3602            double highMem = Runtime.getRuntime().totalMemory();
3603            double freeMem = Runtime.getRuntime().freeMemory();
3604            double usedMem = (highMem - freeMem);
3605            int availableMB = Math.round( ((float)totalMem - (float)usedMem) / 1024f / 1024f);
3606            int percentOfAvailable = Math.round((float)estimatedMB / (float)availableMB * 100f);
3607            if (percentOfAvailable > 95) {
3608                String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>";
3609                message += "which exceeds 95% of total amount available (" + availableMB +"MB).<br>";
3610                message += "Data load cancelled.</html>";
3611                JComponent msgLabel = new JLabel(message);
3612                GuiUtils.showDialog("Data Size", msgLabel);
3613                return;
3614            } else if (percentOfAvailable >= 75) {
3615                String message = "<html>You are attempting to load " + estimatedMB + "MB of data,<br>";
3616                message += percentOfAvailable + "% of the total amount available (" + availableMB + "MB).<br>";
3617                message += "Continue loading data?</html>";
3618                JComponent msgLabel = new JLabel(message);
3619                if (!GuiUtils.askOkCancel("Data Size", msgLabel)) {
3620                    return;
3621                }
3622            }
3623        }
3624        super.processDialog(dcd);
3625    }
3626
3627    /**
3628     * Estimate the number of megabytes that will be used by this data selection
3629     */
3630    protected int getEstimatedMegabytes(DataControlDialog dcd) {
3631        int estimatedMB = 0;
3632        DataChoice dataChoice = dcd.getDataChoice();
3633        if (dataChoice != null) {
3634            Object[] selectedControls = dcd.getSelectedControls();
3635            for (int i = 0; i < selectedControls.length; i++) {
3636                ControlDescriptor cd = (ControlDescriptor) selectedControls[i];
3637
3638                //Check if the data selection is ok
3639                if(!dcd.getDataSelectionWidget().okToCreateTheDisplay(cd.doesLevels())) {
3640                    continue;
3641                }
3642
3643                DataSelection dataSelection = dcd.getDataSelectionWidget().createDataSelection(cd.doesLevels());
3644                                
3645                // Get the size in pixels of the requested image
3646                Object gotSize = dataSelection.getProperty("SIZE");
3647                if (gotSize == null) {
3648                        continue;
3649                }
3650                List<String> dims = StringUtil.split((String)gotSize, " ", false, false);
3651                int myLines = -1;
3652                int myElements = -1;
3653                if (dims.size() == 2) {
3654                    try {
3655                        myLines = Integer.parseInt(dims.get(0));
3656                        myElements = Integer.parseInt(dims.get(1));
3657                    }
3658                    catch (Exception e) { }
3659                }
3660
3661                // Get the count of times requested
3662                int timeCount = 1;
3663                DataSelectionWidget dsw = dcd.getDataSelectionWidget();
3664                List times = dsw.getSelectedDateTimes();
3665                List timesAll = dsw.getAllDateTimes();
3666                if ((times != null) && !times.isEmpty()) {
3667                    timeCount = times.size();
3668                } else if ((timesAll != null) && !timesAll.isEmpty()) {
3669                    timeCount = timesAll.size();
3670                }
3671                
3672                // Total number of pixels
3673                // Assumed lines x elements x times x 4bytes
3674                // Empirically seems to be taking *twice* that (64bit fields??)
3675                float totalPixels = (float)myLines * (float)myElements * (float)timeCount;
3676                float totalBytes = totalPixels * 4 * 2;
3677                estimatedMB += Math.round(totalBytes / 1024f / 1024f);
3678
3679                int additionalMB = 0;
3680                // Empirical tests show that textures are not affecting
3681                // required memory... comment out for now
3682                /*
3683                int textureDimensions = 2048;
3684                int mbPerTexture = Math.round((float)textureDimensions * (float)textureDimensions * 4 / 1024f / 1024f);
3685                int textureCount = (int)Math.ceil((float)myLines / 2048f) * (int)Math.ceil((float)myElements / 2048f);
3686                int additionalMB = textureCount * mbPerTexture * timeCount;
3687                */
3688                estimatedMB += additionalMB;
3689            }
3690        }
3691        return estimatedMB;
3692    }
3693
3694    /**
3695     * Represents a SavedBundle as a tree.
3696     */
3697    private class BundleTreeNode {
3698
3699        private String name;
3700
3701        private SavedBundle bundle;
3702
3703        private List<BundleTreeNode> kids;
3704
3705        /**
3706         * This constructor is used to build a node that is considered a
3707         * "parent." These nodes only have child nodes, no SavedBundles. This
3708         * was done so that distinguishing between bundles and bundle
3709         * subcategories would be easy.
3710         * 
3711         * @param name The name of this node. For a parent node with
3712         *        "Toolbar>cat" as the path, the name parameter would contain
3713         *        only "cat."
3714         */
3715        public BundleTreeNode(String name) {
3716            this(name, null);
3717        }
3718
3719        /**
3720         * Nodes constructed using this constructor can only ever be child
3721         * nodes.
3722         * 
3723         * @param name The name of the SavedBundle.
3724         * @param bundle A reference to the SavedBundle.
3725         */
3726        public BundleTreeNode(String name, SavedBundle bundle) {
3727            this.name = name;
3728            this.bundle = bundle;
3729            kids = new LinkedList<>();
3730        }
3731
3732        /**
3733         * @param child The node to be added to the current node.
3734         */
3735        public void addChild(BundleTreeNode child) {
3736            kids.add(child);
3737        }
3738
3739        /**
3740         * @return Returns all child nodes of this node.
3741         */
3742        public List<BundleTreeNode> getChildren() {
3743            return kids;
3744        }
3745
3746        /**
3747         * @return Return the SavedBundle associated with this node (if any).
3748         */
3749        public SavedBundle getBundle() {
3750            return bundle;
3751        }
3752
3753        /**
3754         * @return The name of this node.
3755         */
3756        public String getName() {
3757            return name;
3758        }
3759    }
3760
3761    /**
3762     * A type of {@code HttpFormEntry} that supports line wrapping for
3763     * text area entries.
3764     * 
3765     * @see HttpFormEntry
3766     */
3767    private static class FormEntry extends HttpFormEntry {
3768        /** Initial contents of this entry. */
3769        private String value = "";
3770
3771        /** Whether or not the JTextArea should wrap lines. */
3772        private boolean wrap = true;
3773
3774        /** Entry type. Used to remain compatible with the IDV. */
3775        private int type = HttpFormEntry.TYPE_AREA;
3776
3777        /** Number of rows in the JTextArea. */
3778        private int rows = 5;
3779
3780        /** Number of columns in the JTextArea. */
3781        private int cols = 30;
3782
3783        /** GUI representation of this entry. */
3784        private JTextArea component = new JTextArea(value, rows, cols);
3785
3786        /**
3787         * Required to keep Java happy.
3788         */
3789        public FormEntry() {
3790            super(HttpFormEntry.TYPE_AREA, "form_data[description]", 
3791                "Description:");
3792        }
3793
3794        /**
3795         * Using this constructor allows McIDAS-V to control whether or not a
3796         * HttpFormEntry performs line wrapping for JTextArea components.
3797         * 
3798         * @see HttpFormEntry#HttpFormEntry(int, String, String, String, int, int, boolean)
3799         */
3800        public FormEntry(boolean wrap, int type, String name, String label, String value, int rows, int cols, boolean required) {
3801            super(type, name, label, value, rows, cols, required);
3802            this.type = type;
3803            this.rows = rows;
3804            this.cols = cols;
3805            this.wrap = wrap;
3806        }
3807
3808        /**
3809         * Overrides the IDV method so that the McIDAS-V support request form
3810         * will wrap lines in the "Description" field.
3811         */
3812        @SuppressWarnings("unchecked")
3813        @Override public void addToGui(List guiComps) {
3814            if (type == HttpFormEntry.TYPE_AREA) {
3815                Dimension minSize = new Dimension(500, 200);
3816                guiComps.add(LayoutUtil.top(GuiUtils.rLabel(getLabel())));
3817                component.setLineWrap(wrap);
3818                component.setWrapStyleWord(wrap);
3819                JScrollPane sp = new JScrollPane(component);
3820                sp.setPreferredSize(minSize);
3821                sp.setMinimumSize(minSize);
3822                guiComps.add(sp);
3823            } else {
3824                super.addToGui(guiComps);
3825            }
3826        }
3827
3828        /**
3829         * Since the IDV doesn't provide a getComponent for 
3830         * {@code addToGui}, we must make our {@code component} field
3831         * local to this class. 
3832         * Hijacks any value requests so that the local {@code component}
3833         * field is queried, not the IDV's.
3834         */
3835        @Override public String getValue() {
3836            if (type != HttpFormEntry.TYPE_AREA) {
3837                return super.getValue();
3838            }
3839            return component.getText();
3840        }
3841
3842        /**
3843         * Hijacks any requests to set the {@code component} field's text.
3844         */
3845        @Override public void setValue(final String newValue) {
3846            if (type == HttpFormEntry.TYPE_AREA) {
3847                component.setText(newValue);
3848            } else {
3849                super.setValue(newValue);
3850            }
3851        }
3852    }
3853
3854    /**
3855     * A {@code ToolbarStyle} is a representation of the way icons associated
3856     * with current toolbar actions should be displayed. This notion is so far
3857     * limited to the sizing of icons, but that may change.
3858     */
3859    public enum ToolbarStyle {
3860        /**
3861         * Represents the current toolbar actions as large icons. Currently,
3862         * {@literal "large"} is defined as {@code 32 x 32} pixels.
3863         */
3864        LARGE("Large Icons", "action.icons.large", 32),
3865
3866        /**
3867         * Represents the current toolbar actions as medium icons. Currently,
3868         * {@literal "medium"} is defined as {@code 22 x 22} pixels.
3869         */
3870        MEDIUM("Medium Icons", "action.icons.medium", 22),
3871
3872        /** 
3873         * Represents the current toolbar actions as small icons. Currently,
3874         * {@literal "small"} is defined as {@code 16 x 16} pixels. 
3875         */
3876        SMALL("Small Icons", "action.icons.small", 16);
3877
3878        /** Label to use in the toolbar customization popup menu. */
3879        private final String label;
3880
3881        /** Signals that the user selected a specific icon size. */
3882        private final String action;
3883
3884        /** Icon dimensions. Each icon should be {@code size * size}. */
3885        private final int size;
3886
3887        /**
3888         * {@link #size} in {@link String} form, merely for use with the IDV's
3889         * preference functionality.
3890         */
3891        private final String sizeAsString;
3892
3893        /**
3894         * Initializes a toolbar style.
3895         * 
3896         * @param label Label used in the toolbar popup menu.
3897         * @param action Command that signals the user selected this toolbar 
3898         * style.
3899         * @param size Dimensions of the icons.
3900         * 
3901         * @throws NullPointerException if {@code label} or {@code action} are
3902         * null.
3903         * 
3904         * @throws IllegalArgumentException if {@code size} is not positive.
3905         */
3906        ToolbarStyle(final String label, final String action, final int size) {
3907            requireNonNull(label, "Label cannot be null.");
3908            requireNonNull(action, "Action cannot be null.");
3909
3910            if (size <= 0) {
3911                throw new IllegalArgumentException("Size must be a positive integer");
3912            }
3913
3914            this.label = label;
3915            this.action = action;
3916            this.size = size;
3917            this.sizeAsString = Integer.toString(size);
3918        }
3919
3920        /**
3921         * Returns the label to use as a brief description of this style.
3922         */
3923        public String getLabel() {
3924            return label;
3925        }
3926
3927        /**
3928         * Returns the action command associated with this style.
3929         */
3930        public String getAction() {
3931            return action;
3932        }
3933
3934        /**
3935         * Returns the dimensions of icons used in this style.
3936         */
3937        public int getSize() {
3938            return size;
3939        }
3940
3941        /**
3942         * Returns {@link #size} as a {@link String} to make cooperating with
3943         * the IDV preferences code easier.
3944         */
3945        public String getSizeAsString() {
3946            return sizeAsString;
3947        }
3948
3949        /**
3950         * Returns a brief description of this ToolbarStyle. A typical 
3951         * example:<br/>
3952         * {@code [ToolbarStyle@1337: label="Large Icons", size=32]}
3953         * 
3954         * <p>Note that the format and details provided are subject to change.
3955         */
3956        public String toString() {
3957            return String.format("[ToolbarStyle@%x: label=%s, size=%d]", 
3958                hashCode(), label, size);
3959        }
3960
3961        /**
3962         * Convenience method for build the toolbar customization popup menu.
3963         * 
3964         * @param manager {@link UIManager} that will be listening for action
3965         * commands.
3966         * 
3967         * @return Menu item that has {@code manager} listening for 
3968         * {@link #action}.
3969         */
3970        protected JMenuItem buildMenuItem(final UIManager manager) {
3971            JMenuItem item = new JRadioButtonMenuItem(label);
3972            item.setActionCommand(action);
3973            item.addActionListener(manager);
3974            return item;
3975        }
3976    }
3977
3978    /**
3979     * Represents what McIDAS-V {@literal "knows"} about IDV actions.
3980     */
3981    protected enum ActionAttribute {
3982
3983        /**
3984         * Unique identifier for an IDV action. Required attribute.
3985         * 
3986         * @see IdvUIManager#ATTR_ID
3987         */
3988        ID(ATTR_ID), 
3989
3990        /**
3991         * Path to an icon for this action. Currently required. Note that 
3992         * McIDAS-V differs from the IDV in that actions must support different
3993         * icon sizes. This is implemented in McIDAS-V by simply having the value
3994         * of this path be a valid {@literal "format string"}, 
3995         * such as {@code image="/edu/wisc/ssec/mcidasv/resources/icons/toolbar/background-image%d.png"}
3996         * 
3997         * <p>The upshot is that this value <b>will not be a valid path in 
3998         * McIDAS-V</b>. Use either {@link IdvAction#getMenuIcon()} or 
3999         * {@link IdvAction#getIconForStyle}.
4000         * 
4001         * @see IdvUIManager#ATTR_IMAGE
4002         * @see IdvAction#getRawIconPath()
4003         * @see IdvAction#getMenuIcon()
4004         * @see IdvAction#getIconForStyle
4005         */
4006        ICON(ATTR_IMAGE), 
4007
4008        /**
4009         * Brief description of a IDV action. Required attribute.
4010         * @see IdvUIManager#ATTR_DESCRIPTION
4011         */
4012        DESCRIPTION(ATTR_DESCRIPTION), 
4013
4014        /**
4015         * Allows actions to be clustered into arbitrary groups. Currently 
4016         * optional; defaults to {@literal "General"}.
4017         * @see IdvUIManager#ATTR_GROUP
4018         */
4019        GROUP(ATTR_GROUP, "General"), 
4020
4021        /**
4022         * Actual method call used to invoke a given IDV action. Required 
4023         * attribute.
4024         * @see IdvUIManager#ATTR_ACTION
4025         */
4026        ACTION(ATTR_ACTION);
4027
4028        /**
4029         * A blank {@link String} if this is a required attribute, or a 
4030         * {@code String} value to use in case this attribute has not been 
4031         * specified by a given IDV action.
4032         */
4033        private final String defaultValue;
4034
4035        /**
4036         * String representation of this attribute as used by the IDV.
4037         * @see #asIdvString()
4038         */
4039        private final String idvString;
4040
4041        /** Whether or not this attribute is required. */
4042        private final boolean required;
4043
4044        /**
4045         * Creates a constant that represents a required IDV action attribute.
4046         * 
4047         * @param idvString Corresponding IDV attribute {@link String}. Cannot be {@code null}.
4048         * 
4049         * @throws NullPointerException if {@code idvString} is {@code null}.
4050         */
4051        ActionAttribute(final String idvString) {
4052            requireNonNull(idvString, "Cannot be associated with a null IDV action attribute String");
4053
4054            this.idvString = idvString; 
4055            this.defaultValue = ""; 
4056            this.required = true; 
4057        }
4058
4059        /**
4060         * Creates a constant that represents an optional IDV action attribute.
4061         * 
4062         * @param idvString Corresponding IDV attribute {@link String}. 
4063         * Cannot be {@code null}.
4064         * @param defValue Default value for actions that do not have this 
4065         * attribute. Cannot be {@code null} or an empty {@code String}.
4066         * 
4067         * @throws NullPointerException if either {@code idvString} or 
4068         * {@code defValue} is {@code null}.
4069         * @throws IllegalArgumentException if {@code defValue} is an empty 
4070         * {@code String}.
4071         * 
4072         */
4073        ActionAttribute(final String idvString, final String defValue) {
4074            requireNonNull(idvString, "Cannot be associated with a null IDV action attribute String");
4075            Contract.notNull(defValue, "Optional action attribute \"%s\" requires a non-null default value", toString());
4076            Contract.checkArg(!defValue.equals(""), "Optional action attribute \"%s\" requires something more descriptive than an empty String", toString());
4077
4078            this.idvString = idvString; 
4079            this.defaultValue = defValue; 
4080            this.required = (defaultValue.equals("")); 
4081        }
4082
4083        /**
4084         * @return The {@link String} representation of this attribute, as is 
4085         * used by the IDV.
4086         * 
4087         * @see IdvUIManager#ATTR_ACTION
4088         * @see IdvUIManager#ATTR_DESCRIPTION
4089         * @see IdvUIManager#ATTR_GROUP
4090         * @see IdvUIManager#ATTR_ID
4091         * @see IdvUIManager#ATTR_IMAGE
4092         */
4093        public String asIdvString() { return idvString; }
4094
4095        /**
4096         * @return {@literal "Default value"} for this attribute. 
4097         * Blank {@link String}s imply that the attribute is required (and 
4098         * thus lacks a true default value).
4099         */
4100        public String defaultValue() { return defaultValue; }
4101
4102        /**
4103         * @return Whether or not this attribute is a required attribute for 
4104         * valid {@link IdvAction}s.
4105         */
4106        public boolean isRequired() { return required; }
4107    }
4108
4109    /**
4110     * Represents the set of known {@link IdvAction}s in an idiom that can be
4111     * easily used by both the IDV and McIDAS-V.
4112     */
4113    // TODO(jon:101): use Sets instead of maps and whatnot
4114    // TODO(jon:103): create an invalid IdvAction
4115    public static final class IdvActions {
4116
4117        /** Maps {@literal "id"} values to {@link IdvAction}s. */
4118        private final Map<String, IdvAction> idToAction = new ConcurrentHashMap<>();
4119
4120        /** Collects {@link IdvAction}s {@literal "under"} common group values. */
4121        // TODO(jon:102): this should probably become concurrency-friendly.
4122        private final Map<String, Set<IdvAction>> groupToActions = new LinkedHashMap<>();
4123
4124        /**
4125         * 
4126         * 
4127         * @param idv Reference to the IDV {@literal "god"} object. Cannot be {@code null}.
4128         * @param collectionId IDV resource collection that contains our actions. Cannot be {@code null}.
4129         * 
4130         * @throws NullPointerException if {@code idv} or {@code collectionId} 
4131         * is {@code null}. 
4132         */
4133        public IdvActions(final IntegratedDataViewer idv, final XmlIdvResource collectionId) {
4134            requireNonNull(idv, "Cannot provide a null IDV reference");
4135            requireNonNull(collectionId, "Cannot build actions from a null collection id");
4136
4137            // i lub u xpath (but how much slower is this?)
4138            String query = "//action[@id and @image and @description and @action]";
4139            for (Element e : elements(idv, collectionId, query)) {
4140                IdvAction a = new IdvAction(e);
4141                String id = a.getAttribute(ActionAttribute.ID);
4142                idToAction.put(id, a);
4143                String group = a.getAttribute(ActionAttribute.GROUP);
4144                if (!groupToActions.containsKey(group)) {
4145                    groupToActions.put(group, new LinkedHashSet<IdvAction>());
4146                }
4147                Set<IdvAction> groupedIds = groupToActions.get(group);
4148                groupedIds.add(a);
4149            }
4150        }
4151
4152        /**
4153         * Attempts to return the {@link IdvAction} associated with the given
4154         * {@code actionId}.
4155         * 
4156         * @param actionId Identifier to use in the search. Cannot be 
4157         * {@code null}.
4158         * 
4159         * @return Either the {@code IdvAction} that matches {@code actionId} 
4160         * or {@code null} if there was no match.
4161         * 
4162         * @throws NullPointerException if {@code actionId} is {@code null}.
4163         */
4164        // TODO(jon:103) here
4165        public IdvAction getAction(final String actionId) {
4166            requireNonNull(actionId, "Null action identifiers are not allowed");
4167            return idToAction.get(actionId);
4168        }
4169
4170        /**
4171         * Searches for the action associated with {@code actionId} and 
4172         * returns the value associated with the given {@link ActionAttribute}.
4173         * 
4174         * @param actionId Identifier to search for. Cannot be {@code null}.
4175         * @param attr Attribute whose value is desired. Cannot be {@code null}.
4176         * 
4177         * @return Either the desired attribute value of the desired action, 
4178         * or {@code null} if {@code actionId} has no associated action.
4179         * 
4180         * @throws NullPointerException if either {@code actionId} or 
4181         * {@code attr} is {@code null}.
4182         */
4183        // TODO(jon:103) here
4184        public String getAttributeForAction(final String actionId, final ActionAttribute attr) {
4185            requireNonNull(actionId, "Null action identifiers are not allowed");
4186            requireNonNull(attr, "Actions cannot have values associated with a null attribute");
4187            IdvAction action = idToAction.get(actionId);
4188            if (action == null) {
4189                return null;
4190            }
4191            return action.getAttribute(attr);
4192        }
4193
4194        /**
4195         * Attempts to return the XML {@link Element} that {@literal "represents"} the
4196         * action associated with {@code actionId}.
4197         * 
4198         * @param actionId Identifier whose XML element is desired. Cannot be {@code null}.
4199         * 
4200         * @return Either the XML element associated with {@code actionId} or {@code null}.
4201         * 
4202         * @throws NullPointerException if {@code actionId} is {@code null}.
4203         * 
4204         * @see IdvAction#originalElement
4205         */
4206        // TODO(jon:103) here
4207        public Element getElementForAction(final String actionId) {
4208            requireNonNull(actionId, "Cannot search for a null action identifier");
4209            IdvAction action = idToAction.get(actionId);
4210            if (action == null) {
4211                return null;
4212            }
4213            return action.getElement();
4214        }
4215
4216        /**
4217         * Attempts to return an {@link Icon} for a given {@link ActionAttribute#ID} and
4218         * {@link ToolbarStyle}.
4219         * 
4220         * @param actionId ID of the action whose {@literal "styled"} icon is 
4221         * desired. Cannot be {@code null}.
4222         * @param style Desired {@code Icon} style. Cannot be {@code null}.
4223         * 
4224         * @return Either the {@code Icon} associated with {@code actionId} 
4225         * and {@code style}, or {@code null}.
4226         * 
4227         * @throws NullPointerException if either {@code actionId} or 
4228         * {@code style} is {@code null}.
4229         */
4230        // TODO(jon:103) here
4231        public Icon getStyledIconFor(final String actionId, final ToolbarStyle style) {
4232            requireNonNull(actionId, "Cannot get an icon for a null action identifier");
4233            requireNonNull(style, "Cannot get an icon for a null ToolbarStyle");
4234            IdvAction a = idToAction.get(actionId);
4235            if (a == null) {
4236                return null;
4237            }
4238            return a.getIconForStyle(style);
4239        }
4240
4241        // TODO(jon:105): replace with something better
4242        public List<String> getAttributes(final ActionAttribute attr) {
4243            requireNonNull(attr, "Actions cannot have null attributes");
4244            List<String> attributeList = arrList(idToAction.size());
4245            for (Map.Entry<String, IdvAction> entry : idToAction.entrySet()) {
4246                attributeList.add(entry.getValue().getAttribute(attr));
4247            }
4248            return attributeList;
4249        }
4250
4251        /**
4252         * @return List of all known {@code IdvAction}s.
4253         */
4254        public List<IdvAction> getAllActions() {
4255            return arrList(idToAction.values());
4256        }
4257
4258        /**
4259         * @return List of all known action groupings.
4260         * 
4261         * @see ActionAttribute#GROUP
4262         * @see #getActionsForGroup(String)
4263         */
4264        public List<String> getAllGroups() {
4265            return arrList(groupToActions.keySet());
4266        }
4267
4268        /**
4269         * Returns the {@link Set} of {@link IdvAction}s associated with the 
4270         * given {@code group}.
4271         * 
4272         * @param group Group whose associated actions you want. Cannot be 
4273         * {@code null}.
4274         * 
4275         * @return Collection of {@code IdvAction}s associated with 
4276         * {@code group}. A blank collection is returned if there are no actions
4277         * associated with {@code group}.
4278         * 
4279         * @throws NullPointerException if {@code group} is {@code null}.
4280         * 
4281         * @see ActionAttribute#GROUP
4282         * @see #getAllGroups()
4283         */
4284        public Set<IdvAction> getActionsForGroup(final String group) {
4285            requireNonNull(group, "Actions cannot be associated with a null group");
4286            if (!groupToActions.containsKey(group)) {
4287                return Collections.emptySet();
4288            }
4289            return groupToActions.get(group);
4290        }
4291
4292        /**
4293         * Returns a summary of the known IDV actions. Please note that this 
4294         * format is subject to change, and is not intended for serialization.
4295         * 
4296         * @return String that looks like 
4297         * {@code [IdvActions@HASHCODE: actions=...]}.
4298         */
4299        @Override public String toString() {
4300            return String.format("[IdvActions@%x: actions=%s]", hashCode(), idToAction);
4301        }
4302    }
4303
4304    /**
4305     * Represents an individual IDV action. Should be fairly adaptable to
4306     * unforeseen changes from Unidata?
4307     */
4308    // TODO(jon:106): Implement equals/hashCode so that you can use these in Sets. The only relevant value should be the id, right?
4309    public static final class IdvAction {
4310
4311        /** The XML {@link Element} that represents this IDV action. */
4312        private final Element originalElement;
4313
4314        /** Mapping of (known) XML attributes to values for this individual action. */
4315        private final Map<ActionAttribute, String> attributes;
4316
4317        /** 
4318         * Simple {@literal "cache"} for the different icons this action has
4319         * displayed. This is {@literal "lazy"}, so the cache does not contain
4320         * icons for {@link ToolbarStyle}s that haven't been used. 
4321         */
4322        private final Map<ToolbarStyle, Icon> iconCache = new ConcurrentHashMap<>();
4323
4324        /**
4325         * Creates a representation of an IDV action using a given {@link Element}.
4326         * 
4327         * @param element XML representation of an IDV action. Cannot be {@code null}.
4328         * 
4329         * @throws NullPointerException if {@code element} is {@code null}.
4330         * @throws IllegalArgumentException if {@code element} is not a valid IDV action.
4331         * 
4332         * @see UIManager#isValidIdvAction(Element)
4333         */
4334        public IdvAction(final Element element) {
4335            requireNonNull(element, "Cannot build an action from a null element");
4336            // TODO(jon:107): need a way to diagnose what's wrong with the action?
4337            Contract.checkArg(isValidIdvAction(element), "Action lacks required attributes");
4338            originalElement = element;
4339            attributes = actionElementToMap(element);
4340        }
4341
4342        /**
4343         * @return Returns the {@literal "raw"} path to the icon associated 
4344         * with this action. Remember that this is actually a {@literal "format string"}
4345         * and should not be considered a valid path! 
4346         * 
4347         * @see #getIconForStyle
4348         */
4349        public String getRawIconPath() {
4350            return attributes.get(ActionAttribute.ICON);
4351        }
4352
4353        /**
4354         * @return Returns the {@link Icon} associated with {@link ToolbarStyle#SMALL}.
4355         */
4356        public Icon getMenuIcon() {
4357            return getIconForStyle(ToolbarStyle.SMALL);
4358        }
4359
4360        /**
4361         * Returns the {@link Icon} associated with this action and the given
4362         * {@link ToolbarStyle}.
4363         * 
4364         * @param style {@literal "Style"} of the {@code Icon} to be returned.
4365         * Cannot be {@code null}.
4366         * 
4367         * @return This action's {@code Icon} with {@code style} {@literal "applied."}
4368         * 
4369         * @see ActionAttribute#ICON
4370         * @see #iconCache
4371         */
4372        public Icon getIconForStyle(final ToolbarStyle style) {
4373            requireNonNull(style, "Cannot build an icon for a null ToolbarStyle");
4374            if (!iconCache.containsKey(style)) {
4375                String styledPath = String.format(getRawIconPath(), style.getSize());
4376                URL tmp = getClass().getResource(styledPath);
4377                iconCache.put(style, new ImageIcon(Toolkit.getDefaultToolkit().getImage(tmp)));
4378            }
4379            return iconCache.get(style);
4380        }
4381
4382        /**
4383         * @return Returns the identifier of this {@code IdvAction}.
4384         */
4385        public String getId() {
4386            return getAttribute(ActionAttribute.ID);
4387        }
4388
4389        /**
4390         * Representation of this {@code IdvAction} as an {@literal "IDV action call"}.
4391         * 
4392         * @return String that is suitable to hand off to the IDV for execution. 
4393         */
4394        public String getCommand() {
4395            return "idv.handleAction('action:"+getAttribute(ActionAttribute.ID)+"')";
4396        }
4397
4398        /**
4399         * Returns the value associated with a given {@link ActionAttribute} 
4400         * for this action.
4401         * 
4402         * @param attr ActionAttribute whose value you want. Cannot be {@code null}.
4403         * 
4404         * @return Value associated with {@code attr}.
4405         * 
4406         * @throws NullPointerException if {@code attr} is {@code null}.
4407         */
4408        public String getAttribute(final ActionAttribute attr) {
4409            requireNonNull(attr, "No values can be associated with a null ActionAttribute");
4410            return attributes.get(attr);
4411        }
4412
4413        /**
4414         * @return The XML {@link Element} used to create this {@code IdvAction}.
4415         */
4416        // TODO(jon:104): any way to copy this element? if so, this can become an immutable class!
4417        public Element getElement() {
4418            return originalElement;
4419        }
4420
4421        /**
4422         * Returns a brief description of this action. Please note that the 
4423         * format is subject to change and is not intended for serialization.
4424         * 
4425         * @return String that looks like {@code [IdvAction@HASHCODE: attributes=...]}.
4426         */
4427        @Override public String toString() {
4428            return String.format("[IdvAction@%x: attributes=%s]", hashCode(), attributes);
4429        }
4430    }
4431}