001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.startupmanager;
030
031import static edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.EMPTY_STRING;
032import static edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.SET_PREFIX;
033
034import java.awt.BorderLayout;
035import java.awt.Color;
036import java.awt.Component;
037import java.awt.Container;
038import java.awt.Dimension;
039import java.awt.FlowLayout;
040import java.awt.Graphics;
041import java.awt.Graphics2D;
042import java.awt.RenderingHints;
043import java.awt.event.ActionEvent;
044import java.awt.event.ActionListener;
045import java.io.BufferedReader;
046import java.io.File;
047import java.io.FileInputStream;
048import java.io.FileOutputStream;
049import java.io.FileReader;
050import java.io.IOException;
051import java.io.InputStream;
052import java.io.OutputStream;
053import java.util.HashMap;
054import java.util.List;
055import java.util.Map;
056import java.util.Objects;
057import java.util.Properties;
058
059import javax.swing.BorderFactory;
060import javax.swing.DefaultListCellRenderer;
061import javax.swing.DefaultListModel;
062import javax.swing.GroupLayout;
063import javax.swing.ImageIcon;
064import javax.swing.JButton;
065import javax.swing.JCheckBox;
066import javax.swing.JComboBox;
067import javax.swing.JComponent;
068import javax.swing.JFrame;
069import javax.swing.JLabel;
070import javax.swing.JList;
071import javax.swing.JOptionPane;
072import javax.swing.JPanel;
073import javax.swing.JScrollPane;
074import javax.swing.JSplitPane;
075import javax.swing.JTextField;
076import javax.swing.JTree;
077import javax.swing.LayoutStyle;
078import javax.swing.ListModel;
079import javax.swing.ListSelectionModel;
080import javax.swing.SwingConstants;
081import javax.swing.WindowConstants;
082import javax.swing.border.EmptyBorder;
083import javax.swing.tree.DefaultMutableTreeNode;
084import javax.swing.tree.DefaultTreeCellRenderer;
085import javax.swing.ToolTipManager;
086
087import edu.wisc.ssec.mcidasv.startupmanager.options.FileOption;
088import edu.wisc.ssec.mcidasv.util.GetMem;
089import ucar.unidata.ui.Help;
090import ucar.unidata.util.GuiUtils;
091import ucar.unidata.util.LogUtil;
092import ucar.unidata.util.StringUtil;
093import edu.wisc.ssec.mcidasv.ArgumentManager;
094import edu.wisc.ssec.mcidasv.Constants;
095import edu.wisc.ssec.mcidasv.startupmanager.options.BooleanOption;
096import edu.wisc.ssec.mcidasv.startupmanager.options.LoggerLevelOption;
097import edu.wisc.ssec.mcidasv.startupmanager.options.MemoryOption;
098import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster;
099import edu.wisc.ssec.mcidasv.startupmanager.options.TextOption;
100import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
101
102/**
103 * Manages the McIDAS-V startup options in a context that is completely free
104 * from the traditional IDV/McIDAS-V overhead.
105 */
106public class StartupManager implements edu.wisc.ssec.mcidasv.Constants {
107    
108    // TODO(jon): replace
109    public static final String[][] PREF_PANELS = {
110        { Constants.PREF_LIST_GENERAL, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/mcidasv-round32.png" },
111        { Constants.PREF_LIST_VIEW, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/tab-new32.png" },
112        { Constants.PREF_LIST_TOOLBAR, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/application-x-executable32.png" },
113        { Constants.PREF_LIST_DATA_CHOOSERS, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/preferences-desktop-remote-desktop32.png" },
114        { Constants.PREF_LIST_ADDE_SERVERS, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/applications-internet32.png" },
115        { Constants.PREF_LIST_AVAILABLE_DISPLAYS, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/video-display32.png" },
116        { Constants.PREF_LIST_NAV_CONTROLS, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/input-mouse32.png" },
117        { Constants.PREF_LIST_FORMATS_DATA,"/edu/wisc/ssec/mcidasv/resources/icons/prefs/preferences-desktop-theme32.png" },
118        { Constants.PREF_LIST_ADVANCED, "/edu/wisc/ssec/mcidasv/resources/icons/prefs/applications-internet32.png" },
119    };
120    
121    // TODO(jon): replace
122    public static final Object[][] RENDER_HINTS = {
123        { RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON },
124        { RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY },
125        { RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON },
126    };
127    
128    /** usage message */
129    public static final String USAGE_MESSAGE =
130        "Usage: runMcV-Prefs <args>";
131    
132    /** Path to the McIDAS-V help set within {@literal mcv_userguide.jar}. */
133    private static final String HELP_PATH = "/docs/userguide";
134    
135    /** ID of the startup prefs help page. */
136    private static final String HELP_TARGET = "idv.tools.preferences.advancedpreferences";
137    
138    /** The type of platform as reported by {@link #determinePlatform()}. */
139    private final Platform platform = determinePlatform();
140    
141    /** Cached copy of the application rendering hints. */
142    public static final RenderingHints HINTS = getRenderingHints();
143    
144    /** Contains the list of the different preference panels. */
145    private final JList panelList = new JList(new DefaultListModel());
146    
147    /** Panel containing the startup options. */
148    private JPanel ADVANCED_PANEL;
149    
150    /**
151     * Panel to use for all other preference panels while running startup 
152     * manager.
153     */
154    private JPanel BAD_CHOICE_PANEL;
155    
156    /** Contains the various buttons (Apply, Ok, Help, Cancel). */
157    private JPanel COMMAND_ROW_PANEL;
158    
159    private static StartupManager instance;
160    
161    private StartupManager() {
162        
163    }
164    
165    public static StartupManager getInstance() {
166        if (instance == null) {
167            instance = new StartupManager();
168        }
169        return instance;
170    }
171    
172    /**
173     * Creates and returns the rendering hints for the GUI. 
174     * Built from {@link #RENDER_HINTS}
175     * 
176     * @return Hints to use when displaying the GUI.
177     */
178    public static RenderingHints getRenderingHints() {
179        RenderingHints hints = new RenderingHints(null);
180        for (int i = 0; i < RENDER_HINTS.length; i++)
181            hints.put(RENDER_HINTS[i][0], RENDER_HINTS[i][1]);
182        return hints;
183    }
184    
185    /**
186     * Figures out the type of platform. Queries the {@literal "os.name"}
187     * system property to determine the platform type.
188     * 
189     * @return {@link Platform#UNIXLIKE}, {@link Platform#WINDOWS},
190     * or {@link Platform#MAC}.
191     */
192    private Platform determinePlatform() {
193        String os = System.getProperty("os.name");
194        if (os == null) {
195            throw new RuntimeException("Java could not determine operating system!");
196        }
197
198        Platform p;
199        if (os.startsWith("Windows")) {
200            p = Platform.WINDOWS;
201        } else if (os.startsWith("Mac")) {
202            p = Platform.MAC;
203        } else if (os.startsWith("Linux")) {
204            p = Platform.UNIXLIKE;
205        } else {
206            throw new RuntimeException("Unsupported operating system '"+os+'\'');
207        }
208        return p;
209    }
210    
211    /** 
212     * Returns either {@link Platform#UNIXLIKE}, {@link Platform#WINDOWS},
213     * or {@link Platform#MAC}.
214     * 
215     * @return The platform as determined by {@link #determinePlatform()}.
216     */
217    public Platform getPlatform() {
218        return platform;
219    }
220    
221    /**
222     * Saves the changes to the preferences and quits. Unlike the other button
223     * handling methods, this one is public. This was done so that the advanced
224     * preferences (within McIDAS-V) can force an update to the startup prefs.
225     */
226    public void handleApply() {
227        OptionMaster.getInstance().writeStartup();
228    }
229    
230    /**
231     * Saves the preference changes.
232     */
233    protected void handleOk() {
234        OptionMaster.getInstance().writeStartup();
235        System.exit(0);
236    }
237    
238    /** 
239     * Shows the startup preferences help page.
240     */
241    protected void handleHelp() {
242        Help.setTopDir(HELP_PATH);
243        Help.getDefaultHelp().gotoTarget(HELP_TARGET);
244    }
245    
246    /**
247     * Simply quits the program.
248     */
249    protected void handleCancel() {
250        System.exit(0);
251    }
252    
253    /**
254     * Returns the preferences panel that corresponds with the user's 
255     * {@code JList} selection.
256     * 
257     * <p>In the context of the startup manager, this means that any 
258     * {@code JList} selection <i>other than</i> {@literal "Advanced"} will
259     * return the results of {@link #getUnavailablePanel()}. Otherwise the
260     * results of {@link #getAdvancedPanel(boolean)} will be returned.
261     * 
262     * @return Either the advanced preferences panel or an 
263     * {@literal "unavailable"}, depending upon the user's selection.
264     */
265    private Container getSelectedPanel() {
266        ListModel listModel = panelList.getModel();
267        int index = panelList.getSelectedIndex();
268        if (index == -1) {
269            return getAdvancedPanel(true);
270        }
271        String key = ((JLabel)listModel.getElementAt(index)).getText();
272        if (!Constants.PREF_LIST_ADVANCED.equals(key)) {
273            return getUnavailablePanel();
274        }
275        return getAdvancedPanel(true);
276    }
277    
278    /**
279     * Creates and returns a dummy panel.
280     * 
281     * @return Panel containing only a note about 
282     * &quot;options unavailable.&quot;
283     */
284    private JPanel buildUnavailablePanel() {
285        JPanel panel = new JPanel();
286        panel.add(new JLabel("These options are unavailable in this context"));
287        return panel;
288    }
289    
290    /**
291     * Creates and returns the advanced preferences panel.
292     * 
293     * @return Panel with all the various startup options.
294     */
295    private JPanel buildAdvancedPanel() {
296        OptionMaster optMaster = OptionMaster.getInstance();
297        MemoryOption heapSize = optMaster.getMemoryOption("HEAP_SIZE");
298        BooleanOption jogl = optMaster.getBooleanOption("JOGL_TOGL");
299        BooleanOption use3d = optMaster.getBooleanOption("USE_3DSTUFF");
300        BooleanOption defaultBundle = optMaster.getBooleanOption("DEFAULT_LAYOUT");
301        BooleanOption useNpot = optMaster.getBooleanOption("USE_NPOT");
302        BooleanOption useGeometryByRef = optMaster.getBooleanOption("USE_GEOBYREF");
303        BooleanOption useImageByRef = optMaster.getBooleanOption("USE_IMAGEBYREF");
304        FileOption startupBundle = optMaster.getFileOption("STARTUP_BUNDLE");
305        TextOption jvmArgs = optMaster.getTextOption("JVM_OPTIONS");
306        LoggerLevelOption logLevel = optMaster.getLoggerLevelOption("LOG_LEVEL");
307        TextOption textureWidth = optMaster.getTextOption("TEXTURE_WIDTH");
308        TextOption scaling = optMaster.getTextOption("MCV_SCALING");
309        BooleanOption darkMode = optMaster.getBooleanOption("USE_DARK_MODE");
310        
311        JPanel startupPanel = new JPanel();
312        startupPanel.setBorder(BorderFactory.createTitledBorder("Startup Options"));
313        
314        // Build the memory panel
315        JPanel heapPanel = McVGuiUtils.makeLabeledComponent(heapSize.getLabel()+':', heapSize.getComponent());
316        
317        // Build the 3D panel
318        JCheckBox use3dCheckBox = use3d.getComponent();
319        use3dCheckBox.setText(use3d.getLabel());
320        final JCheckBox joglCheckBox = jogl.getComponent();
321        joglCheckBox.setText(jogl.getLabel());
322        JPanel texturePanel = McVGuiUtils.makeLabeledComponent(textureWidth.getLabel()+':', textureWidth.getComponent());
323//        JTextField textureField = textureWidth.getComponent();
324
325        JPanel internalPanel = McVGuiUtils.topBottom(use3dCheckBox, joglCheckBox, McVGuiUtils.Prefer.TOP);
326        JPanel j3dPanel = McVGuiUtils.makeLabeledComponent("3D:", internalPanel);
327        
328        // Build the bundle panel
329        JComponent startupBundlePanel = startupBundle.getComponent();
330        JCheckBox defaultBundleCheckBox = defaultBundle.getComponent();
331        defaultBundleCheckBox.setText(defaultBundle.getLabel());
332        JPanel bundlePanel = McVGuiUtils.makeLabeledComponent(startupBundle.getLabel()+ ':',
333            McVGuiUtils.topBottom(startupBundlePanel, defaultBundleCheckBox, McVGuiUtils.Prefer.TOP));
334
335        JCheckBox useGeometryByRefCheckBox = useGeometryByRef.getComponent();
336        useGeometryByRefCheckBox.setText(useGeometryByRef.getLabel());
337        
338        JCheckBox useImageByRefCheckBox = useImageByRef.getComponent();
339        useImageByRefCheckBox.setText(useImageByRef.getLabel());
340        
341        JCheckBox useNpotCheckBox = useNpot.getComponent();
342        useNpotCheckBox.setText(useNpot.getLabel());
343
344        JCheckBox useDarkModeCheckBox = darkMode.getComponent();
345        useDarkModeCheckBox.setText(darkMode.getLabel());
346        if (platform != Platform.MAC) {
347            ToolTipManager.sharedInstance().setInitialDelay(0);
348            useDarkModeCheckBox.setToolTipText("Dark Mode not yet supported on this operating system");
349        }
350
351        // this is a JComboBox<String>; kinda struggling to represent this
352        // in java's type system.
353        JComboBox logLevelComboBox = logLevel.getComponent();
354
355        JPanel logLevelPanel = McVGuiUtils.makeLabeledComponent(logLevel.getLabel()+':', logLevelComboBox);
356        
357        JPanel scalingPanel = McVGuiUtils.makeLabeledComponent(scaling.getLabel()+':', scaling.getComponent());
358
359        JPanel miscPanel = McVGuiUtils.makeLabeledComponent("Misc:", McVGuiUtils.vertical(useDarkModeCheckBox, scalingPanel));
360
361        JTextField jvmArgsField = jvmArgs.getComponent();
362
363        JButton warningBtn = McVGuiUtils.makeImageButton(Constants.ICON_EXCLAMATION_SMALL, "Warning");
364        warningBtn.addActionListener(e -> {
365            JOptionPane.showMessageDialog(null, "This option refers to official Java VM options. " +
366                    "Modifications with invalid arguments could break your install.", "Warning", JOptionPane.OK_OPTION);
367        });
368
369        JPanel jvmPanel = new JPanel(new BorderLayout());
370                jvmPanel.add(McVGuiUtils.makeLabeledComponent("Java Flags:", jvmArgsField), BorderLayout.CENTER);
371                jvmPanel.add(warningBtn, BorderLayout.EAST);
372
373        // TJJ Nov 2018 
374        // Add note at top of Startup Options alerting user a restart may be needed
375        JLabel restartLabel = new JLabel("Note: Most startup options require a McIDAS-V restart to take effect");
376        restartLabel.setForeground(Color.red);
377        restartLabel.setBorder(new EmptyBorder(6, 6, 6, 6));
378
379        Component[] visadComponents = {
380            useGeometryByRefCheckBox,
381            useImageByRefCheckBox,
382            useNpotCheckBox,
383            texturePanel,
384        };
385        
386        JPanel visadPanel = McVGuiUtils.makeLabeledComponent("VisAD:", McVGuiUtils.vertical(visadComponents));
387        
388        GroupLayout panelLayout = new GroupLayout(startupPanel);
389        startupPanel.setLayout(panelLayout);
390        panelLayout.setHorizontalGroup(
391            panelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
392                .addComponent(restartLabel)
393                .addComponent(heapPanel)
394                .addComponent(j3dPanel)
395                .addComponent(bundlePanel)
396                .addComponent(visadPanel)
397                .addComponent(logLevelPanel)
398                .addComponent(miscPanel)
399                .addComponent(jvmPanel)
400        );
401        panelLayout.setVerticalGroup(
402            panelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
403            .addGroup(panelLayout.createSequentialGroup()
404                .addComponent(restartLabel)
405                .addComponent(heapPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
406                .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
407                .addComponent(bundlePanel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
408                .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
409                .addComponent(j3dPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
410                .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
411                .addComponent(visadPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
412                .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
413                .addComponent(logLevelPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
414                .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
415                .addComponent(miscPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
416                .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
417                .addComponent(jvmPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
418            )
419        );
420        return startupPanel;
421    }
422    
423    /**
424     * Builds and returns a {@link JPanel} containing the various buttons that
425     * control the startup manager. These buttons offer identical 
426     * functionality to those built by the IDV's preference manager code.
427     * 
428     * @return A {@code JPanel} containing the following types of buttons:
429     * {@link ApplyButton}, {@link OkButton}, {@link HelpButton}, 
430     * and {@link CancelButton}.
431     * 
432     * @see GuiUtils#makeApplyOkHelpCancelButtons(ActionListener)
433     */
434    private JPanel buildCommandRow() {
435        JPanel panel = new JPanel(new FlowLayout());
436        // Apply doesn't really mean anything in standalone mode...
437//        panel.add(new ApplyButton());
438        OkButton okBtn = new OkButton();
439        okBtn.setToolTipText("Accept changed preferences and close this window");
440        HelpButton hlpBtn = new HelpButton();
441        hlpBtn.setToolTipText("Open the User Guide Page for User Preferences");
442        CancelButton cnclBtn = new CancelButton();
443        cnclBtn.setToolTipText("Exit User Preferences");
444        panel.add(okBtn);
445        panel.add(hlpBtn);
446        panel.add(cnclBtn);
447        panel = McVGuiUtils.makePrettyButtons(panel);
448        return panel;
449    }
450    
451    /**
452     * Returns the advanced preferences panel. Differs from the 
453     * {@link #buildAdvancedPanel()} in that a panel isn't created, unless
454     * {@code forceBuild} is {@code true}.
455     * 
456     * @param forceBuild Always rebuilds the advanced panel if {@code true}.
457     * 
458     * @return Panel containing the startup options.
459     */
460    public JPanel getAdvancedPanel(final boolean forceBuild) {
461        if (forceBuild || (ADVANCED_PANEL == null)) {
462            OptionMaster.getInstance().readStartup();
463            ADVANCED_PANEL = buildAdvancedPanel();
464        }
465        return ADVANCED_PANEL;
466    }
467    
468    public JPanel getUnavailablePanel() {
469        if (BAD_CHOICE_PANEL == null) {
470            BAD_CHOICE_PANEL = buildUnavailablePanel();
471        }
472        return BAD_CHOICE_PANEL;
473    }
474    
475    /**
476     * Returns a panel containing the Apply/Ok/Help/Cancel buttons.
477     * 
478     * @return Panel containing the command row.
479     */
480    public JPanel getCommandRow() {
481        if (COMMAND_ROW_PANEL == null) {
482            COMMAND_ROW_PANEL = buildCommandRow();
483        }
484        return COMMAND_ROW_PANEL;
485    }
486    
487    /**
488     * Build and display the startup manager window.
489     */
490    protected void createDisplay() {
491        DefaultListModel listModel = (DefaultListModel)panelList.getModel();
492
493        for (String[] PREF_PANEL : PREF_PANELS) {
494            ImageIcon icon = new ImageIcon(getClass().getResource(PREF_PANEL[1]));
495            JLabel label = new JLabel(PREF_PANEL[0], icon, SwingConstants.LEADING);
496            listModel.addElement(label);
497        }
498        
499        JScrollPane scroller = new JScrollPane(panelList);
500        final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
501        splitPane.setResizeWeight(0.0);
502        splitPane.setLeftComponent(scroller);
503        scroller.setMinimumSize(new Dimension(166, 319));
504        
505        panelList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
506        panelList.setSelectedIndex(PREF_PANELS.length - 1);
507        panelList.setVisibleRowCount(PREF_PANELS.length);
508        panelList.setCellRenderer(new IconCellRenderer());
509        
510        panelList.addListSelectionListener(e -> {
511            if (!e.getValueIsAdjusting()) {
512                splitPane.setRightComponent(getSelectedPanel());
513            }
514        });
515        
516        splitPane.setRightComponent(getSelectedPanel());
517        
518        JFrame frame = new JFrame("User Preferences");
519        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
520        frame.getContentPane().add(splitPane);
521        frame.getContentPane().add(getCommandRow(), BorderLayout.PAGE_END);
522        
523        frame.pack();
524        frame.setVisible(true);
525    }
526    
527    /**
528     * Copies a file.
529     * 
530     * @param src The file to copy.
531     * @param dst The path to the copy of {@code src}.
532     * 
533     * @throws IOException If there was a problem while attempting to copy.
534     */
535    public void copy(final File src, final File dst) throws IOException {
536        try (
537            InputStream in = new FileInputStream(src);
538            OutputStream out = new FileOutputStream(dst)
539        ) {
540            byte[] buf = new byte[1024];
541            int length;
542            while ((length = in.read(buf)) > 0) {
543                out.write(buf, 0, length);
544            }
545        }
546    }
547    
548    public static class TreeCellRenderer extends DefaultTreeCellRenderer {
549        @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
550            super.getTreeCellRendererComponent(tree, value, sel, expanded,
551                leaf, row, hasFocus);
552                
553            DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
554            
555            File f = (File)node.getUserObject();
556            String path = f.getPath();
557            
558            if (f.isDirectory()) {
559                setToolTipText("Bundle Directory: " + path);
560            } else if (ArgumentManager.isZippedBundle(path)) {
561                setToolTipText("Zipped Bundle: " + path);
562            } else if (ArgumentManager.isXmlBundle(path)) {
563                setToolTipText("XML Bundle: " + path);
564            } else {
565                setToolTipText("Unknown file type: " + path);
566            }
567            setText(f.getName().replace(f.getParent(), ""));
568            return this;
569        }
570    }
571    
572    public static class IconCellRenderer extends DefaultListCellRenderer {
573        @Override public Component getListCellRendererComponent(JList list, 
574            Object value, int index, boolean isSelected, boolean cellHasFocus) 
575        {
576            super.getListCellRendererComponent(list, value, index, isSelected, 
577                cellHasFocus);
578                
579            if (value instanceof JLabel) {
580                setText(((JLabel)value).getText());
581                setIcon(((JLabel)value).getIcon());
582            }
583            
584            return this;
585        }
586        
587        @Override protected void paintComponent(Graphics g) {
588            Graphics2D g2d = (Graphics2D)g;
589            g2d.setRenderingHints(StartupManager.HINTS);
590            super.paintComponent(g2d);
591        }
592    }
593    
594    private static abstract class CommandButton extends JButton 
595        implements ActionListener 
596    {
597        public CommandButton(final String label) {
598            super(label);
599            McVGuiUtils.setComponentWidth(this);
600            addActionListener(this);
601        }
602        
603        @Override public void paintComponent(Graphics g) {
604            Graphics2D g2d = (Graphics2D)g;
605            g2d.setRenderingHints(StartupManager.HINTS);
606            super.paintComponent(g2d);
607        }
608        
609        abstract public void actionPerformed(final ActionEvent e);
610    }
611    
612    private static class ApplyButton extends CommandButton {
613        public ApplyButton() {
614            super("Apply");
615        }
616        public void actionPerformed(final ActionEvent e) {
617            StartupManager.getInstance().handleApply();
618        }
619    }
620    
621    private static class OkButton extends CommandButton {
622        public OkButton() {
623            super("OK");
624        }
625        public void actionPerformed(final ActionEvent e) {
626            StartupManager.getInstance().handleOk();
627        }
628    }
629    
630    private static class HelpButton extends CommandButton {
631        public HelpButton() {
632            super("Help");
633        }
634        public void actionPerformed(final ActionEvent e) {
635            StartupManager.getInstance().handleHelp();
636        }
637    }
638    
639    private static class CancelButton extends CommandButton {
640        public CancelButton() {
641            super("Cancel");
642        }
643        public void actionPerformed(final ActionEvent e) {
644            StartupManager.getInstance().handleCancel();
645        }
646    }
647    
648    public static Properties getDefaultProperties() {
649        Properties props = new Properties();
650        String osName = System.getProperty("os.name");
651        if (osName.startsWith("Mac OS X")) {
652            props.setProperty("userpath", String.format("%s%s%s%s%s", System.getProperty("user.home"), File.separator, "Documents", File.separator, Constants.USER_DIRECTORY_NAME));
653        } else {
654            props.setProperty("userpath", String.format("%s%s%s", System.getProperty("user.home"), File.separator, Constants.USER_DIRECTORY_NAME));
655        }
656        props.setProperty(Constants.PROP_SYSMEM, "0");
657        return props;
658    }
659    
660    /**
661     * Extract any command-line properties and their corresponding values.
662     * 
663     * <p>May print out usage information if a badly formatted 
664     * {@literal "property=value"} pair is encountered, or when an unknown 
665     * argument is found (depending on value of the {@code ignoreUnknown} 
666     * parameter). 
667     * 
668     * <p><b>NOTE:</b> {@code null} is not a permitted value for any parameter.
669     * 
670     * @param ignoreUnknown Whether or not to handle unknown arguments.
671     * @param fromStartupManager Whether or not this call originated from 
672     * {@code startupmanager.jar}.
673     * @param args Array containing command-line arguments.
674     * @param defaults Default parameter values.
675     * 
676     * @return Command-line arguments as a collection of property identifiers
677     * and values.
678     */
679    public static Properties getArgs(final boolean ignoreUnknown, 
680        final boolean fromStartupManager, final String[] args, 
681        final Properties defaults) 
682    {
683        Properties props = new Properties(defaults);
684        for (int i = 0; i < args.length; i++) {
685            
686            // handle property definitions
687            if (args[i].startsWith("-D")) {
688                List<String> l = StringUtil.split(args[i].substring(2), "=");
689                if (l.size() == 2) {
690                    props.setProperty(l.get(0), l.get(1));
691                } else {
692                    usage("Invalid property:" + args[i]);
693                }
694            }
695            
696            // handle userpath changes
697            else if (ARG_USERPATH.equals(args[i]) && ((i + 1) < args.length)) {
698                props.setProperty("userpath", args[++i]);
699            }
700            
701            // handle help requests
702            else if (ARG_HELP.equals(args[i]) && (fromStartupManager)) {
703                System.err.println(USAGE_MESSAGE);
704                System.err.println(getUsageMessage());
705                System.exit(1);
706            }
707            
708            // bail out for unknown args, unless we don't care!
709            else if (!ignoreUnknown){
710                usage("Unknown argument: " + args[i]);
711            }
712        }
713        return props;
714    }
715    
716    public static int getMaximumHeapSize() {
717        int sysmem =
718            StartupManager.getInstance().getPlatform().getAvailableMemory();
719        if ((sysmem > Constants.MAX_MEMORY_32BIT) &&
720            (!System.getProperty("os.arch").contains("64")))
721        {
722            return Constants.MAX_MEMORY_32BIT;
723        }
724        return sysmem;
725    }
726    
727    /**
728     * Print out the command line usage message and exit. Taken entirely from
729     * {@link ucar.unidata.idv.ArgsManager}.
730     * 
731     * @param err The usage message
732     */
733    private static void usage(final String err) {
734        String msg = USAGE_MESSAGE;
735        msg = msg + '\n' + getUsageMessage();
736        LogUtil.userErrorMessage(err + '\n' + msg);
737        System.exit(1);
738    }
739    
740    /**
741     * Return the command line usage message.
742     * 
743     * @return The usage message
744     */
745    protected static String getUsageMessage() {
746        return '\t'+ARG_HELP+"  (this message)\n"+
747               '\t'+ARG_USERPATH+"  <user directory to use>\n"+
748               "\t-Dpropertyname=value  (Define the property value)\n";
749    }
750    
751    /**
752     * Applies the command line arguments to the startup preferences.
753     *
754     * This method is mostly useful because it allows us to supply an
755     * arbitrary {@code args} array, link in
756     * {@link edu.wisc.ssec.mcidasv.McIDASV#main(String[])}.
757     * 
758     * @param ignoreUnknown If {@code true} ignore any parameters that do not 
759     *                      apply to the startup manager. If {@code false},
760     *                      the non-applicable parameters should signify an
761     *                      error.
762     * @param fromStartupManager Whether or not this call originated from the 
763     *                           startup manager (rather than preferences).
764     * @param args Incoming command line arguments. Cannot be {@code null}.
765     * 
766     * @throws NullPointerException if {@code args} is null.
767     * 
768     * @see #getArgs(boolean, boolean, String[], Properties)
769     */
770    public static void applyArgs(final boolean ignoreUnknown,
771                                 final boolean fromStartupManager,
772                                 final String[] args)
773        throws IllegalArgumentException
774    {
775        Objects.requireNonNull(args, "Argument list cannot be null");
776
777        StartupManager sm = StartupManager.getInstance();
778        Platform platform = sm.getPlatform();
779        
780        Properties props = getArgs(ignoreUnknown,
781                                   fromStartupManager,
782                                   args,
783                                   getDefaultProperties());
784        platform.setUserDirectory(props.getProperty("userpath"));
785        platform.setAvailableMemory(GetMem.getMemory());
786    }
787    
788    /**
789     * Extracts all startup preferences and returns them in a convenient 
790     * {@code Map}.
791     * 
792     * @return Either a {@link HashMap} mapping {@literal "preference ID"} to 
793     *         its corresponding value, or an empty map.
794     */
795    public static Map<String, String> getStartupPrefs() {
796        StartupManager sm = StartupManager.getInstance();
797        int size = OptionMaster.getInstance().blahblah.length;
798        File script = new File(sm.getPlatform().getUserPrefs());
799        Map<String, String> startupPrefs = new HashMap<>(size);
800        try (BufferedReader br = new BufferedReader(new FileReader(script))) {
801            String line;
802            while ((line = br.readLine()) != null) {
803                if (line.startsWith("#")) {
804                    continue;
805                }
806                if (line.startsWith(SET_PREFIX)) {
807                    line = line.replace(SET_PREFIX, EMPTY_STRING);
808                }
809                int splitAt = line.indexOf('=');
810                if (splitAt >= 0) {
811                    String k = line.substring(0, splitAt);
812                    String v = line.substring(splitAt + 1);
813                    startupPrefs.put(k, v);
814                }
815            }
816        } catch (IOException e) {
817            System.err.println("Problem reading from '"+script.getPath()+"': "+e.getMessage());
818        }
819        return startupPrefs;
820    }
821    
822    public static void main(String[] args) {
823        applyArgs(false, true, args);
824        StartupManager.getInstance().createDisplay();
825    }
826}