001/*
002 * $Id: StartupManager.java,v 1.64 2011/03/24 16:06:34 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.startupmanager;
032
033import java.awt.BorderLayout;
034import java.awt.Component;
035import java.awt.Container;
036import java.awt.Dimension;
037import java.awt.FlowLayout;
038import java.awt.Graphics;
039import java.awt.Graphics2D;
040import java.awt.RenderingHints;
041import java.awt.event.ActionEvent;
042import java.awt.event.ActionListener;
043import java.io.File;
044import java.io.FileInputStream;
045import java.io.FileOutputStream;
046import java.io.IOException;
047import java.io.InputStream;
048import java.io.OutputStream;
049import java.util.List;
050import java.util.Properties;
051
052import javax.swing.BorderFactory;
053import javax.swing.DefaultListCellRenderer;
054import javax.swing.DefaultListModel;
055import javax.swing.ImageIcon;
056import javax.swing.JButton;
057import javax.swing.JCheckBox;
058import javax.swing.JFrame;
059import javax.swing.JLabel;
060import javax.swing.JList;
061import javax.swing.JPanel;
062import javax.swing.JScrollPane;
063import javax.swing.JSplitPane;
064import javax.swing.JTree;
065import javax.swing.ListModel;
066import javax.swing.ListSelectionModel;
067import javax.swing.SwingConstants;
068import javax.swing.event.ListSelectionEvent;
069import javax.swing.event.ListSelectionListener;
070import javax.swing.tree.DefaultMutableTreeNode;
071import javax.swing.tree.DefaultTreeCellRenderer;
072
073import ucar.unidata.ui.Help;
074import ucar.unidata.util.GuiUtils;
075import ucar.unidata.util.LogUtil;
076import ucar.unidata.util.StringUtil;
077import edu.wisc.ssec.mcidasv.ArgumentManager;
078import edu.wisc.ssec.mcidasv.Constants;
079import edu.wisc.ssec.mcidasv.startupmanager.options.BooleanOption;
080import edu.wisc.ssec.mcidasv.startupmanager.options.DirectoryOption;
081import edu.wisc.ssec.mcidasv.startupmanager.options.MemoryOption;
082import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster;
083import edu.wisc.ssec.mcidasv.startupmanager.options.TextOption;
084import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
085
086// using an enum to enforce singleton-ness is a hack, but it's been pretty 
087// effective. OptionMaster is used in a similar way. The remaining enums are 
088// used in the more traditional fashion.
089public enum StartupManager implements edu.wisc.ssec.mcidasv.Constants {
090    /** Lone instance of the startup manager. */
091    INSTANCE;
092
093    // TODO(jon): replace
094    public static final String[][] PREF_PANELS = {
095        { Constants.PREF_LIST_GENERAL, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/mcidasv-round32.png" },
096        { Constants.PREF_LIST_VIEW, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/tab-new32.png" },
097        { Constants.PREF_LIST_TOOLBAR, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/application-x-executable32.png" },
098        { Constants.PREF_LIST_DATA_CHOOSERS, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/preferences-desktop-remote-desktop32.png" },
099        { Constants.PREF_LIST_ADDE_SERVERS, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/applications-internet32.png" },
100        { Constants.PREF_LIST_AVAILABLE_DISPLAYS, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/video-display32.png" },
101        { Constants.PREF_LIST_NAV_CONTROLS, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/input-mouse32.png" },
102        { Constants.PREF_LIST_FORMATS_DATA,"/edu/wisc/ssec/mcidasv/resources/icons/prefs/preferences-desktop-theme32.png" },
103        { Constants.PREF_LIST_ADVANCED, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/applications-internet32.png" },
104    };
105
106    // TODO(jon): replace
107    public static final Object[][] RENDER_HINTS = {
108        { RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON },
109        { RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY },
110        { RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON },
111    };
112
113    public enum Platform {
114        /** Instance of unix-specific platform information. */
115        UNIXLIKE("/", "runMcV.prefs", "\n"),
116
117        /** Instance of windows-specific platform information. */
118        WINDOWS("\\", "runMcV-Prefs.bat", "\r\n");
119
120        /** Path to the user's {@literal "userpath"} directory. */
121        private String userDirectory;
122
123        /** The path to the user's copy of the startup preferences. */
124        private String userPrefs;
125
126        /** Path to the preference file that ships with McIDAS-V. */
127        private final String defaultPrefs;
128
129        /** Holds the platform's representation of a new line. */
130        private final String newLine;
131
132        /** Directory delimiter for the current platform. */
133        private final String pathSeparator;
134
135        /** Total amount of memory avilable in megabytes */
136        private int availableMemory = 0;
137
138        /**
139         * Initializes the platform-specific paths to the different files 
140         * required by the startup manager.
141         * 
142         * @param pathSeparator Character that delimits directories. On Windows
143         * this will be {@literal \\}, while on Unix-style systems, it will be
144         * {@literal /}.
145         * @param defaultPrefs The path to the preferences file that ships with
146         * McIDAS-V.
147         * @param newLine Character(s!) that represent a new line for this 
148         * platform.
149         * 
150         * @throws NullPointerException if either {@code pathSeparator} or 
151         * {@code defaultPrefs} are null.
152         * 
153         * @throws IllegalArgumentException if either {@code pathSeparator} or
154         * {@code defaultPrefs} are an empty string.
155         */
156        Platform(final String pathSeparator, final String defaultPrefs, 
157            final String newLine) 
158        {
159            if (pathSeparator == null || defaultPrefs == null)
160                throw new NullPointerException("");
161            if (pathSeparator.length() == 0 || defaultPrefs.length() == 0)
162                throw new IllegalArgumentException("");
163
164            String osName = System.getProperty("os.name");
165            if (osName.startsWith("Mac OS X")) {
166                this.userDirectory = System.getProperty("user.home") + pathSeparator + "Documents" + pathSeparator + Constants.USER_DIRECTORY_NAME;             
167            }
168            else {
169                this.userDirectory = System.getProperty("user.home") + pathSeparator + Constants.USER_DIRECTORY_NAME;                   
170            }
171            this.userPrefs = userDirectory + pathSeparator + defaultPrefs;
172            this.defaultPrefs = defaultPrefs;
173            this.newLine = newLine;
174            this.pathSeparator = pathSeparator;
175        }
176
177        /**
178         * Sets the path to the user's userpath directory explicitly.
179         * 
180         * @param path New path.
181         */
182        public void setUserDirectory(final String path) {
183            userDirectory = path;
184            userPrefs = userDirectory + pathSeparator + defaultPrefs;
185        }
186
187        /**
188         * Sets the amount of available memory. {@code megabytes} must be 
189         * greater than or equal to zero.
190         * 
191         * @param megabytes Memory in megabytes
192         * 
193         * @throws NullPointerException if {@code megabytes} is {@code null}.
194         * @throws IllegalArgumentException if {@code megabytes} is less than
195         * zero or does not represent an integer.
196         * 
197         * @see StartupManager#getArgs(String[], Properties)
198         */
199        public void setAvailableMemory(String megabytes) {
200            if (megabytes == null)
201                throw new NullPointerException("Available memory cannot be null");
202            if (megabytes.equals("")) megabytes="0";
203
204            try {
205                int test = Integer.parseInt(megabytes);
206                if (test < 0) 
207                    throw new IllegalArgumentException("Available memory must be a non-negative integer, not \""+megabytes+"\"");
208
209                availableMemory = test;
210            } catch (NumberFormatException e) {
211                throw new IllegalArgumentException("Could not convert \""+megabytes+"\" to a non-negative integer");
212            }
213        }
214
215        /**
216         * Returns the path to the user's {@literal "userpath"} directory.
217         * 
218         * @return Path to the user's directory.
219         */
220        public String getUserDirectory() {
221            return userDirectory;
222        }
223        
224        /**
225         * Returns the path to a file in the user's {@literal "userpath"} directory
226         * 
227         * @param filename
228         * @return Path to a file in the user's directory.
229         */
230        public String getUserFile(String filename) {
231                return getUserDirectory()+pathSeparator+filename;
232        }
233
234        public String getUserBundles() {
235            return getUserDirectory()+pathSeparator+"bundles";
236        }
237
238        /**
239         * Returns the amount of available memory in megabytes
240         * 
241         * @return Available memory in megabytes
242         */
243        public int getAvailableMemory() {
244            return availableMemory;
245        }
246
247        /**
248         * Returns the path of user's copy of the startup preferences.
249         * 
250         * @return Path to the user's startup preferences file.
251         */
252        public String getUserPrefs() {
253            return userPrefs;
254        }
255
256        /**
257         * Returns the path of the startup preferences included in the McIDAS-V
258         * distribution. Mostly useful for normalizing the user 
259         * directory.
260         * 
261         * @return Path to the default startup preferences.
262         * 
263         * @see OptionMaster#normalizeUserDirectory()
264         */
265        public String getDefaultPrefs() {
266            return defaultPrefs;
267        }
268
269        /**
270         * Returns the platform's notion of a new line.
271         * 
272         * @return Unix-like: {@literal \n}; Windows: {@literal \r\n}.
273         */
274        public String getNewLine() {
275            return newLine;
276        }
277
278        /**
279         * Returns a brief summary of the platform specific file locations. 
280         * Please note that the format and contents are subject to change.
281         * 
282         * @return String that looks like 
283         * {@code [Platform@HASHCODE: defaultPrefs=..., userDirectory=..., 
284         * userPrefs=...]}
285         */
286        @Override public String toString() {
287            return String.format(
288                "[Platform@%x: defaultPrefs=%s, userDirectory=%s, userPrefs=%s]",
289                hashCode(), defaultPrefs, userDirectory, userPrefs);
290        }
291    }
292
293    /** usage message */
294    public static final String USAGE_MESSAGE =
295        "Usage: runMcV-Prefs <args>";
296
297    /** Path to the McIDAS-V help set within {@literal mcv_userguide.jar}. */
298    private static final String HELP_PATH = "/docs/userguide";
299
300    /** ID of the startup prefs help page. */
301    private static final String HELP_TARGET = "idv.tools.preferences.advancedpreferences";
302
303    /** The type of platform as reported by {@link #determinePlatform()}. */
304    private final Platform platform = determinePlatform();
305
306    /** Cached copy of the application rendering hints. */
307    public static final RenderingHints HINTS = getRenderingHints();
308
309    /** Contains the list of the different preference panels. */
310    private final JList panelList = new JList(new DefaultListModel());
311
312    /** Panel containing the startup options. */
313    private JPanel ADVANCED_PANEL;
314
315    /**
316     * Panel to use for all other preference panels while running startup 
317     * manager.
318     */
319    private JPanel BAD_CHOICE_PANEL;
320
321    /** Contains the various buttons (Apply, Ok, Help, Cancel). */
322    private JPanel COMMAND_ROW_PANEL;
323
324    /**
325     * Creates and returns the rendering hints for the GUI. 
326     * Built from {@link #RENDER_HINTS}
327     * 
328     * @return Hints to use when displaying the GUI.
329     */
330    public static RenderingHints getRenderingHints() {
331        RenderingHints hints = new RenderingHints(null);
332        for (int i = 0; i < RENDER_HINTS.length; i++)
333            hints.put(RENDER_HINTS[i][0], RENDER_HINTS[i][1]);
334        return hints;
335    }
336
337    /**
338     * Figures out the type of platform. Queries the &quot;os.name&quot; 
339     * system property to determine the platform type.
340     * 
341     * @return {@link Platform#UNIXLIKE} or {@link Platform#WINDOWS}.
342     */
343    private Platform determinePlatform() {
344        String os = System.getProperty("os.name");
345        if (os == null)
346            throw new RuntimeException();
347
348        if (os.startsWith("Windows"))
349            return Platform.WINDOWS;
350        else
351            return Platform.UNIXLIKE;
352    }
353
354    /** 
355     * Returns either {@link Platform#UNIXLIKE} or 
356     * {@link Platform#WINDOWS}.
357     * 
358     * @return The platform as determined by {@link #determinePlatform()}.
359     */
360    public Platform getPlatform() {
361        return platform;
362    }
363
364    /**
365     * Saves the changes to the preferences and quits. Unlike the other button
366     * handling methods, this one is public. This was done so that the advanced
367     * preferences (within McIDAS-V) can force an update to the startup prefs.
368     */
369    public void handleApply() {
370        OptionMaster.INSTANCE.writeStartup();
371    }
372
373    /**
374     * Saves the preference changes.
375     */
376    protected void handleOk() {
377        OptionMaster.INSTANCE.writeStartup();
378        System.exit(0);
379    }
380
381    /** 
382     * Shows the startup preferences help page.
383     */
384    protected void handleHelp() {
385        Help.setTopDir(HELP_PATH);
386        Help.getDefaultHelp().gotoTarget(HELP_TARGET);
387    }
388
389    /**
390     * Simply quits the program.
391     */
392    protected void handleCancel() {
393        System.exit(0);
394    }
395
396    /**
397     * 
398     * @return
399     */
400    private Container getSelectedPanel() {
401        ListModel listModel = panelList.getModel();
402        int index = panelList.getSelectedIndex();
403        if (index == -1)
404            return getAdvancedPanel(true);
405
406        String key = ((JLabel)listModel.getElementAt(index)).getText();
407        if (!key.equals(Constants.PREF_LIST_ADVANCED))
408            return getUnavailablePanel();
409
410        return getAdvancedPanel(true);
411    }
412
413    /**
414     * Creates and returns a dummy panel.
415     * 
416     * @return Panel containing only a note about 
417     * &quot;options unavailable.&quot;
418     */
419    private JPanel buildUnavailablePanel() {
420        JPanel panel = new JPanel();
421        panel.add(new JLabel("These options are unavailable in this context"));
422        return panel;
423    }
424
425    /**
426     * Creates and returns the advanced preferences panel.
427     * 
428     * @return Panel with all of the various startup options.
429     */
430    private JPanel buildAdvancedPanel() {
431        OptionMaster optMaster = OptionMaster.INSTANCE;
432        MemoryOption heapSize = (MemoryOption)optMaster.getOption("HEAP_SIZE");
433        BooleanOption jogl = (BooleanOption)optMaster.getOption("JOGL_TOGL");
434        BooleanOption use3d = (BooleanOption)optMaster.getOption("USE_3DSTUFF");
435        BooleanOption defaultBundle = (BooleanOption)optMaster.getOption("DEFAULT_LAYOUT");
436        BooleanOption useDirect3d = (BooleanOption)optMaster.getOption("D3DREND");
437        BooleanOption useCmsCollector = (BooleanOption)optMaster.getOption("USE_CMSGC");
438        BooleanOption useNpot = (BooleanOption)optMaster.getOption("USE_NPOT");
439        BooleanOption useGeometryByRef = (BooleanOption)optMaster.getOption("USE_GEOBYREF");
440        BooleanOption useImageByRef = (BooleanOption)optMaster.getOption("USE_IMAGEBYREF");
441        DirectoryOption startupBundle = (DirectoryOption)optMaster.getOption("STARTUP_BUNDLE");
442        TextOption jvmArgs = (TextOption)optMaster.getOption("JVM_ARGS");
443
444        JPanel startupPanel = new JPanel();
445        startupPanel.setBorder(BorderFactory.createTitledBorder("Startup Options"));
446
447        // Build the memory panel
448        JPanel heapPanel = McVGuiUtils.makeLabeledComponent(heapSize.getLabel()+":", heapSize.getComponent());
449
450        // Build the 3D panel
451        JCheckBox use3dCheckBox = (JCheckBox)use3d.getComponent();
452        use3dCheckBox.setText(use3d.getLabel());
453        final JCheckBox joglCheckBox = (JCheckBox)jogl.getComponent();
454        joglCheckBox.setText(jogl.getLabel());
455        final JCheckBox direct3dBox = (JCheckBox)useDirect3d.getComponent();
456        direct3dBox.setText(useDirect3d.getLabel());
457
458        JPanel internalPanel = McVGuiUtils.topCenterBottom(use3dCheckBox, joglCheckBox, direct3dBox);
459        JPanel j3dPanel = McVGuiUtils.makeLabeledComponent("3D:", internalPanel);
460
461        // Build the bundle panel
462        JPanel startupBundlePanel = (JPanel)startupBundle.getComponent();
463        JCheckBox defaultBundleCheckBox = (JCheckBox)defaultBundle.getComponent();
464        defaultBundleCheckBox.setText(defaultBundle.getLabel());
465        JPanel bundlePanel = McVGuiUtils.makeLabeledComponent(startupBundle.getLabel()+":",
466            McVGuiUtils.topBottom(startupBundlePanel, defaultBundleCheckBox, McVGuiUtils.Prefer.TOP));
467        
468        JCheckBox useCmsCollectorCheckBox = (JCheckBox)useCmsCollector.getComponent();
469        useCmsCollectorCheckBox.setText(useCmsCollector.getLabel());
470
471        JCheckBox useGeometryByRefCheckBox = (JCheckBox)useGeometryByRef.getComponent();
472        useGeometryByRefCheckBox.setText(useGeometryByRef.getLabel());
473
474        JCheckBox useImageByRefCheckBox = (JCheckBox)useImageByRef.getComponent();
475        useImageByRefCheckBox.setText(useImageByRef.getLabel());
476
477        JCheckBox useNpotCheckBox = (JCheckBox)useNpot.getComponent();
478        useNpotCheckBox.setText(useNpot.getLabel());
479        
480        JPanel miscPanel = McVGuiUtils.makeLabeledComponent("Misc:", useCmsCollectorCheckBox);
481
482        Component[] visadComponents = new Component[] {
483                        useGeometryByRefCheckBox,
484                        useImageByRefCheckBox,
485                        useNpotCheckBox
486        };
487
488        JPanel visadPanel = McVGuiUtils.makeLabeledComponent("VisAD:", McVGuiUtils.vertical(visadComponents));
489
490        javax.swing.GroupLayout panelLayout = new javax.swing.GroupLayout(startupPanel);
491        startupPanel.setLayout(panelLayout);
492        panelLayout.setHorizontalGroup(
493            panelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
494                .addComponent(heapPanel)
495                .addComponent(j3dPanel)
496                .addComponent(bundlePanel)
497                .addComponent(miscPanel)
498                .addComponent(visadPanel)
499        );
500        panelLayout.setVerticalGroup(
501            panelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
502            .addGroup(panelLayout.createSequentialGroup()
503                .addComponent(heapPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
504                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
505                .addComponent(bundlePanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
506                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
507                .addComponent(j3dPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
508                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
509                .addComponent(visadPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
510                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
511                .addComponent(miscPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
512            )
513        );
514
515        return startupPanel;
516    }
517
518    /**
519     * Builds and returns a {@link JPanel} containing the various buttons that
520     * control the startup manager. These buttons offer identical 
521     * functionality to those built by the IDV's preference manager code.
522     * 
523     * @return A {@code JPanel} containing the following types of buttons:
524     * {@link ApplyButton}, {@link OkButton}, {@link HelpButton}, 
525     * and {@link CancelButton}.
526     * 
527     * @see GuiUtils#makeApplyOkHelpCancelButtons(ActionListener)
528     */
529    private JPanel buildCommandRow() {
530        JPanel panel = new JPanel(new FlowLayout());
531        // Apply doesn't really mean anything in standalone mode...
532//        panel.add(new ApplyButton());
533        panel.add(new OkButton());
534        panel.add(new HelpButton());
535        panel.add(new CancelButton());
536        panel = McVGuiUtils.makePrettyButtons(panel);
537        return panel;
538    }
539
540    /**
541     * Returns the advanced preferences panel. Differs from the 
542     * {@link #buildAdvancedPanel()} in that a panel isn't created, unless
543     * {@code forceBuild} is {@code true}.
544     * 
545     * @param forceBuild Always rebuilds the advanced panel if {@code true}.
546     * 
547     * @return Panel containing the startup options.
548     */
549    public JPanel getAdvancedPanel(final boolean forceBuild) {
550        if (forceBuild || ADVANCED_PANEL == null) {
551            OptionMaster.INSTANCE.readStartup();
552            ADVANCED_PANEL = buildAdvancedPanel();
553        }
554        return ADVANCED_PANEL;
555    }
556
557    public JPanel getUnavailablePanel() {
558        if (BAD_CHOICE_PANEL == null)
559            BAD_CHOICE_PANEL = buildUnavailablePanel();
560        return BAD_CHOICE_PANEL;
561    }
562
563    /**
564     * Returns a panel containing the Apply/Ok/Help/Cancel buttons.
565     * 
566     * @return Panel containing the the command row.
567     */
568    public JPanel getCommandRow() {
569        if (COMMAND_ROW_PANEL == null)
570            COMMAND_ROW_PANEL = buildCommandRow();
571        return COMMAND_ROW_PANEL;
572    }
573
574    /**
575     * Build and display the startup manager window.
576     */
577    protected void createDisplay() {
578        DefaultListModel listModel = (DefaultListModel)panelList.getModel();
579
580        for (int i = 0; i < PREF_PANELS.length; i++) {
581            ImageIcon icon = new ImageIcon(getClass().getResource(PREF_PANELS[i][1]));
582            JLabel label = new JLabel(PREF_PANELS[i][0], icon, SwingConstants.LEADING);
583            listModel.addElement(label);
584        }
585
586        JScrollPane scroller = new JScrollPane(panelList);
587        final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
588        splitPane.setResizeWeight(0.0);
589        splitPane.setLeftComponent(scroller);
590        scroller.setMinimumSize(new Dimension(166, 319));
591
592        panelList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
593        panelList.setSelectedIndex(PREF_PANELS.length - 1);
594        panelList.setVisibleRowCount(PREF_PANELS.length);
595        panelList.setCellRenderer(new IconCellRenderer());
596
597        panelList.addListSelectionListener(new ListSelectionListener() {
598            public void valueChanged(final ListSelectionEvent e) {
599                if (!e.getValueIsAdjusting())
600                    splitPane.setRightComponent(getSelectedPanel());
601            }
602        });
603
604        splitPane.setRightComponent(getSelectedPanel());
605
606        JFrame frame = new JFrame("User Preferences");
607        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
608        frame.getContentPane().add(splitPane);
609        frame.getContentPane().add(getCommandRow(), BorderLayout.PAGE_END);
610
611        frame.pack();
612        frame.setVisible(true);
613    }
614
615    /**
616     * Copies a file.
617     * 
618     * @param src The file to copy.
619     * @param dst The path to the copy of {@code src}.
620     * 
621     * @throws IOException If there was a problem while attempting to copy.
622     */
623    public void copy(final File src, final File dst) throws IOException {
624        InputStream in = new FileInputStream(src);
625        OutputStream out = new FileOutputStream(dst);
626
627        byte[] buf = new byte[1024];
628        int length;
629
630        while ((length = in.read(buf)) > 0)
631            out.write(buf, 0, length);
632
633        in.close();
634        out.close();
635    }
636
637    public static class TreeCellRenderer extends DefaultTreeCellRenderer {
638        @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
639            super.getTreeCellRendererComponent(tree, value, sel, expanded,
640                leaf, row, hasFocus);
641
642            DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
643
644            File f = (File)node.getUserObject();
645            String path = f.getPath();
646
647            if (f.isDirectory())
648                setToolTipText("Bundle Directory: " + path);
649            else if (ArgumentManager.isZippedBundle(path))
650                setToolTipText("Zipped Bundle: " + path);
651            else if (ArgumentManager.isXmlBundle(path))
652                setToolTipText("XML Bundle: " + path);
653            else
654                setToolTipText("Unknown file type: " + path);
655
656            setText(f.getName().replace(f.getParent(), ""));
657            return this;
658        }
659    }
660
661    public static class IconCellRenderer extends DefaultListCellRenderer {
662        @Override public Component getListCellRendererComponent(JList list, 
663            Object value, int index, boolean isSelected, boolean cellHasFocus) 
664        {
665            super.getListCellRendererComponent(list, value, index, isSelected, 
666                cellHasFocus);
667
668            if (value instanceof JLabel) {
669                setText(((JLabel)value).getText());
670                setIcon(((JLabel)value).getIcon());
671            }
672
673            return this;
674        }
675
676        @Override protected void paintComponent(Graphics g) {
677            Graphics2D g2d = (Graphics2D)g;
678            g2d.setRenderingHints(StartupManager.HINTS);
679            super.paintComponent(g2d);
680        }
681    }
682
683    private static abstract class CommandButton extends JButton 
684        implements ActionListener 
685    {
686        public CommandButton(final String label) {
687            super(label);
688            McVGuiUtils.setComponentWidth(this);
689            addActionListener(this);
690        }
691
692        @Override public void paintComponent(Graphics g) {
693            Graphics2D g2d = (Graphics2D)g;
694            g2d.setRenderingHints(StartupManager.HINTS);
695            super.paintComponent(g2d);
696        }
697
698        abstract public void actionPerformed(final ActionEvent e);
699    }
700
701    private static class ApplyButton extends CommandButton {
702        public ApplyButton() {
703            super("Apply");
704        }
705        public void actionPerformed(final ActionEvent e) {
706            StartupManager.INSTANCE.handleApply();
707        }
708    }
709
710    private static class OkButton extends CommandButton {
711        public OkButton() {
712            super("OK");
713        }
714        public void actionPerformed(final ActionEvent e) {
715            StartupManager.INSTANCE.handleOk();
716        }
717    }
718
719    private static class HelpButton extends CommandButton {
720        public HelpButton() {
721            super("Help");
722        }
723        public void actionPerformed(final ActionEvent e) {
724            StartupManager.INSTANCE.handleHelp();
725        }
726    }
727
728    private static class CancelButton extends CommandButton {
729        public CancelButton() {
730            super("Cancel");
731        }
732        public void actionPerformed(final ActionEvent e) {
733            StartupManager.INSTANCE.handleCancel();
734        }
735    }
736
737    public static Properties getDefaultProperties() {
738        Properties props = new Properties();
739        String osName = System.getProperty("os.name");
740        if (osName.startsWith("Mac OS X")) {
741            props.setProperty("userpath", String.format("%s%s%s%s%s", System.getProperty("user.home"), File.separator, "Documents", File.separator, Constants.USER_DIRECTORY_NAME));
742        }
743        else {
744            props.setProperty("userpath", String.format("%s%s%s", System.getProperty("user.home"), File.separator, Constants.USER_DIRECTORY_NAME));
745        }
746        props.setProperty(Constants.PROP_SYSMEM, "0");
747        return props;
748    }
749
750    public static Properties getArgs(final boolean ignoreUnknown, 
751        final boolean fromStartupManager, final String[] args, 
752        final Properties defaults) 
753    {
754        Properties props = new Properties(defaults);
755        for (int i = 0; i < args.length; i++) {
756
757            // handle property definitions
758            if (args[i].startsWith("-D")) {
759                List<String> l = StringUtil.split(args[i].substring(2), "=");
760                if (l.size() == 2)
761                    props.setProperty(l.get(0), l.get(1));
762                else
763                    usage("Invalid property:" + args[i]);
764            }
765
766            // handle userpath changes
767            else if (ARG_USERPATH.equals(args[i]) && (i+1) < args.length) {
768                props.setProperty("userpath", args[++i]);
769            }
770
771            // handle help requests
772            else if (args[i].equals(ARG_HELP) && (fromStartupManager)) {
773                System.err.println(USAGE_MESSAGE);
774                System.err.println(getUsageMessage());
775                System.exit(1);
776            }
777
778            // bail out for unknown args, unless we don't care!
779            else if (!ignoreUnknown){
780                usage("Unknown argument: " + args[i]);
781            }
782        }
783        return props;
784    }
785
786    public static int getMaximumHeapSize() {
787        int sysmem = StartupManager.INSTANCE.getPlatform().getAvailableMemory();
788        if (sysmem > Constants.MAX_MEMORY_32BIT &&
789                        System.getProperty("os.arch").indexOf("64") < 0)
790                return Constants.MAX_MEMORY_32BIT;
791        return sysmem;
792    }
793
794    /**
795     * Print out the command line usage message and exit. Taken entirely from
796     * {@link ucar.unidata.idv.ArgsManager}.
797     * 
798     * @param err The usage message
799     */
800    private static void usage(final String err) {
801        String msg = USAGE_MESSAGE;
802        msg = msg + "\n" + getUsageMessage();
803        LogUtil.userErrorMessage(err + "\n" + msg);
804        System.exit(1);
805    }
806
807    /**
808     * Return the command line usage message.
809     * 
810     * @return The usage message
811     */
812    protected static String getUsageMessage() {
813        return "\t"+ARG_HELP+"  (this message)\n"+
814               "\t"+ARG_USERPATH+"  <user directory to use>\n"+
815               "\t-Dpropertyname=value  (Define the property value)\n";
816    }
817
818    /**
819     * Applies the command line arguments to the startup preferences. This function
820     * is mostly useful because it allows us to supply an arbitrary {@code args} array,
821     * link in {@link edu.wisc.ssec.mcidasv.McIDASV#main(String[])}.
822     * 
823     * @param ignoreUnknown If {@code true} ignore any parameters that do not 
824     * apply to the startup manager. If {@code false} the non-applicable 
825     * parameters should signify an error.
826     * @param fromStartupManager Whether or not this call originated from the 
827     * startup manager (rather than preferences).
828     * @param args Incoming command line arguments. Cannot be {@code null}.
829     * 
830     * @throws NullPointerException if {@code args} is null.
831     * 
832     * @see #getArgs(boolean, String[], Properties)
833     */
834    public static void applyArgs(final boolean ignoreUnknown, final boolean fromStartupManager, final String[] args) {
835        if (args == null)
836            throw new NullPointerException("Arguments list cannot be null");
837        StartupManager sm = StartupManager.INSTANCE;
838        Platform platform = sm.getPlatform();
839
840        Properties props = getArgs(ignoreUnknown, fromStartupManager, args, getDefaultProperties());
841        platform.setUserDirectory(props.getProperty("userpath"));
842        platform.setAvailableMemory(props.getProperty(Constants.PROP_SYSMEM));
843    }
844
845    public static void main(String[] args) {
846        applyArgs(false, true, args);
847        StartupManager.INSTANCE.createDisplay();
848    }
849}