001    /*
002     * $Id: StartupManager.java,v 1.65 2012/02/19 17:35:49 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
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    
031    package edu.wisc.ssec.mcidasv.startupmanager;
032    
033    import java.awt.BorderLayout;
034    import java.awt.Component;
035    import java.awt.Container;
036    import java.awt.Dimension;
037    import java.awt.FlowLayout;
038    import java.awt.Graphics;
039    import java.awt.Graphics2D;
040    import java.awt.RenderingHints;
041    import java.awt.event.ActionEvent;
042    import java.awt.event.ActionListener;
043    import java.io.File;
044    import java.io.FileInputStream;
045    import java.io.FileOutputStream;
046    import java.io.IOException;
047    import java.io.InputStream;
048    import java.io.OutputStream;
049    import java.util.List;
050    import java.util.Properties;
051    
052    import javax.swing.BorderFactory;
053    import javax.swing.DefaultListCellRenderer;
054    import javax.swing.DefaultListModel;
055    import javax.swing.ImageIcon;
056    import javax.swing.JButton;
057    import javax.swing.JCheckBox;
058    import javax.swing.JFrame;
059    import javax.swing.JLabel;
060    import javax.swing.JList;
061    import javax.swing.JPanel;
062    import javax.swing.JScrollPane;
063    import javax.swing.JSplitPane;
064    import javax.swing.JTree;
065    import javax.swing.ListModel;
066    import javax.swing.ListSelectionModel;
067    import javax.swing.SwingConstants;
068    import javax.swing.event.ListSelectionEvent;
069    import javax.swing.event.ListSelectionListener;
070    import javax.swing.tree.DefaultMutableTreeNode;
071    import javax.swing.tree.DefaultTreeCellRenderer;
072    
073    import ucar.unidata.ui.Help;
074    import ucar.unidata.util.GuiUtils;
075    import ucar.unidata.util.LogUtil;
076    import ucar.unidata.util.StringUtil;
077    import edu.wisc.ssec.mcidasv.ArgumentManager;
078    import edu.wisc.ssec.mcidasv.Constants;
079    import edu.wisc.ssec.mcidasv.startupmanager.options.BooleanOption;
080    import edu.wisc.ssec.mcidasv.startupmanager.options.DirectoryOption;
081    import edu.wisc.ssec.mcidasv.startupmanager.options.MemoryOption;
082    import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster;
083    import edu.wisc.ssec.mcidasv.startupmanager.options.TextOption;
084    import 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.
089    public 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    }