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