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