// Copyright (c) 2004-2005 Sun Microsystems, Inc. All rights reserved. Use is
// subject to license terms.
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the Lesser GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
// USA

package org.jdesktop.jdic.screensaver;

import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Settings dialog for screensaver settings.  The dialog is constructed
 * based on the DOM for a screensaver configuration file.  This is necessary
 * for non xscreensaver-based implementations (e.g. win32) so that
 * screensavers can still be configured.
 * 
 * See xscreensaver source distribution hacks/config/README for details.
 *
 * You must use NetBeans to edit this dialog, in order to preserve the
 * integrity of the auto-generated code.
 *
 * @author Mark Roth
 */
class SettingsDialog extends javax.swing.JDialog {
    /** Logging support */
    private static Logger logger = 
        Logger.getLogger("org.jdesktop.jdic.screensaver");
    
    /** Creates new form Settings Dialog with default parameters */
    public SettingsDialog(String configData) {
        super(new JFrame(), true);
        try {
            this.configFile = DocumentBuilderFactory.newInstance().
                newDocumentBuilder().parse(
                new ByteArrayInputStream(configData.getBytes()));
            initComponents();
            processConfigFile();
            setSize(640,480);
            setLocationRelativeTo(null);
            setVisible(true);
        }
        catch(ParserConfigurationException e) {
            logger.log(Level.SEVERE, 
                "Could not create XML parser for config data", e);
        }
        catch(SAXException e) {
            logger.log(Level.SEVERE,
                "Could not parse XML config data: " + configData, e);
        }
        catch(IOException e) {
            logger.log(Level.SEVERE,
                "Could not read XML config data: " + configData, e);
        }
    }
    
    /** Creates new form Settings Dialog */
    public SettingsDialog(java.awt.Frame parent, boolean modal, Document configFile) 
    {
        super(parent, modal);
        this.configFile = configFile;
        initComponents();
        processConfigFile();
    }
    
    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:initComponents
    private void initComponents() {
        java.awt.GridBagConstraints gridBagConstraints;

        pnlButtons = new javax.swing.JPanel();
        btnCancel = new javax.swing.JButton();
        btnOK = new javax.swing.JButton();
        pnlCenter = new javax.swing.JPanel();
        pnlLeft = new javax.swing.JPanel();
        spSettings = new javax.swing.JScrollPane();
        pnlSettings = new javax.swing.JPanel();
        pnlRight = new javax.swing.JPanel();
        spDocumentation = new javax.swing.JScrollPane();
        taDocumentation = new javax.swing.JTextArea();

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        setTitle("Screensaver Settings");
        pnlButtons.setLayout(new java.awt.GridBagLayout());

        pnlButtons.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 5, 5, 5));
        btnCancel.setMnemonic('c');
        btnCancel.setText("Cancel");
        btnCancel.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                btnCancelActionPerformed(evt);
            }
        });

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST;
        gridBagConstraints.weightx = 1.0;
        pnlButtons.add(btnCancel, gridBagConstraints);

        btnOK.setMnemonic('o');
        btnOK.setText("OK");
        btnOK.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                btnOKActionPerformed(evt);
            }
        });

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.insets = new java.awt.Insets(0, 5, 0, 0);
        pnlButtons.add(btnOK, gridBagConstraints);

        getContentPane().add(pnlButtons, java.awt.BorderLayout.SOUTH);

        pnlCenter.setLayout(new java.awt.GridLayout(1, 2, 5, 0));

        pnlCenter.setBorder(javax.swing.BorderFactory.createEmptyBorder(5, 5, 5, 5));
        pnlLeft.setLayout(new java.awt.BorderLayout());

        pnlLeft.setBorder(javax.swing.BorderFactory.createTitledBorder(javax.swing.BorderFactory.createEtchedBorder(), "Settings"));
        spSettings.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 5, 5, 5));
        spSettings.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        pnlSettings.setLayout(new javax.swing.BoxLayout(pnlSettings, javax.swing.BoxLayout.Y_AXIS));

        spSettings.setViewportView(pnlSettings);

        pnlLeft.add(spSettings, java.awt.BorderLayout.CENTER);

        pnlCenter.add(pnlLeft);

        pnlRight.setLayout(new java.awt.BorderLayout());

        pnlRight.setBorder(javax.swing.BorderFactory.createTitledBorder(javax.swing.BorderFactory.createEtchedBorder(), "Documentation"));
        spDocumentation.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 5, 5, 5));
        spDocumentation.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        taDocumentation.setEditable(false);
        taDocumentation.setLineWrap(true);
        taDocumentation.setWrapStyleWord(true);
        taDocumentation.setOpaque(false);
        spDocumentation.setViewportView(taDocumentation);

        pnlRight.add(spDocumentation, java.awt.BorderLayout.CENTER);

        pnlCenter.add(pnlRight);

        getContentPane().add(pnlCenter, java.awt.BorderLayout.CENTER);

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void btnCancelActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnCancelActionPerformed
        dispose();
    }//GEN-LAST:event_btnCancelActionPerformed

    private void btnOKActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnOKActionPerformed
        settings.loadFromCommandline(getNormalizedCommandline());
        settings.saveSettings(screensaverName);
        dispose();
    }//GEN-LAST:event_btnOKActionPerformed
    
    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) throws Exception {
        Document configFile = DocumentBuilderFactory.newInstance().
            newDocumentBuilder().parse(new File(args[0]));
        SettingsDialog d = new SettingsDialog( new JFrame(), true, 
            configFile);
        d.setSize(640,480);
        d.setLocationRelativeTo(null);
        d.setVisible(true);
    }
    
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton btnCancel;
    private javax.swing.JButton btnOK;
    private javax.swing.JPanel pnlButtons;
    private javax.swing.JPanel pnlCenter;
    private javax.swing.JPanel pnlLeft;
    private javax.swing.JPanel pnlRight;
    private javax.swing.JPanel pnlSettings;
    private javax.swing.JScrollPane spDocumentation;
    private javax.swing.JScrollPane spSettings;
    private javax.swing.JTextArea taDocumentation;
    // End of variables declaration//GEN-END:variables
    
    private Document configFile;
    private ArrayList commandline = new ArrayList();
    private JFileChooser chooser = null;
    private String screensaverName;
    private ScreensaverSettings settings;
    private JPanel currentPanel;
    
    /**
     * Reads through the config file and adds controls accordingly.
     */
    private void processConfigFile() {
        Element root = configFile.getDocumentElement();
        
        // Get name of screensaver.
        //
        // From xscreensaver/hacks/config/README:
        //   <screensaver name="PROGRAM-NAME" _label="PRETTY NAME">
        //     ...
        //   </screensaver>
        this.screensaverName = root.getAttribute("name");
        settings = new ScreensaverSettings();
        settings.loadSettings(this.screensaverName);
        String name = root.getAttribute("_label");
        if(name == null) {
            name = root.getAttribute("name");
            if(name == null) {
                name = "Screensaver";
            }
        }
        setTitle(name + " Settings");
        ((TitledBorder)pnlRight.getBorder()).setTitle(name);
        
        // Process each child element:
        pnlSettings.setLayout(new GridBagLayout());
        GridBagConstraints gbc1 = new GridBagConstraints();
        gbc1.fill = GridBagConstraints.HORIZONTAL;
        gbc1.gridwidth = GridBagConstraints.REMAINDER;
        gbc1.weightx = 1.0;
        gbc1.weighty = 1.0;
        gbc1.anchor = GridBagConstraints.NORTH;
        
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(1, 1));
        pnlSettings.add(panel, gbc1);
        this.currentPanel = panel;
        processVGroup(root);
    }
    
    /**
     * Process all child elements of the given root element.
     */
    private void processChildElements(Element root) {
        NodeList children = root.getChildNodes();
        for(int i = 0; i < children.getLength(); i++ ) {
            Node n = children.item(i);
            // Ignore everything but elements:
            if(n instanceof Element) {
                Element e = (Element)n;
                String tag = e.getTagName();
                boolean addSeparator = false;
                if(tag.equals("_description")) {
                    processDescriptionElement(e);
                }
                else if(tag.equals("command")) {
                    processCommandElement(e);
                }
                else if(tag.equals("boolean")) {
                    processBooleanElement(e);
                    addSeparator = true;
                }
                else if(tag.equals("number")) {
                    processNumberElement(e);
                    addSeparator = true;
                }
                else if(tag.equals("select")) {
                    processSelectElement(e);
                    addSeparator = true;
                }
                else if(tag.equals("string")) {
                    processStringElement(e);
                    addSeparator = true;
                }
                else if(tag.equals("file")) {
                    processFileElement(e);
                    addSeparator = true;
                }
                else if(tag.equals("hgroup")) {
                    processHGroup(e);
                }
                else if(tag.equals("vgroup")) {
                    processVGroup(e);
                }
                
                // If true, add a separator between elements
                if(addSeparator) {
                    currentPanel.add(Box.createRigidArea(
                        new Dimension(5, 5)));
                }
            }
        }
    }
    
    /**
     * From xscreensaver/hacks/config/README:
     *
     *  <hgroup>
     *    [ ... <boolean>s ... ]
     *    [ ... <number>s ... ]
     *    [ ... <select>s ... ]
     *    [ ... <string>s ... ]
     *    [ ... <file>s ... ]
     *    [ ... <vgroup>s ... ]
     *  </hgroup>
     *
     *  A horizontal group of widgets/groups.  No more than 4 widgets 
     *  or groups should be used in a row.
     */
    private void processHGroup(Element e) {
        JPanel previousPanel = this.currentPanel;
        this.currentPanel = new JPanel();
        this.currentPanel.setLayout(new BoxLayout(this.currentPanel,
            BoxLayout.X_AXIS));
        processChildElements(e);
        previousPanel.add(this.currentPanel);
        this.currentPanel = previousPanel;
    }
    
    /**
     * From xscreensaver/hacks/config/README:
     *
     *  <vgroup>
     *    [ ... <boolean>s ... ]
     *    [ ... <number>s ... ]
     *    [ ... <select>s ... ]
     *    [ ... <string>s ... ]
     *    [ ... <file>s ... ]
     *    [ ... <vgroup>s ... ]
     *  </vgroup>
     *
     *  A vertical group of widgets/groups.  No more than 10 widgets 
     *  or groups should be used in a column.
     *  
     *  Since the default alignment of widgets is a column, the 
     *  <vgroup> element is only of use inside an <hgroup> element.
     */
    private void processVGroup(Element e) {
        JPanel previousPanel = this.currentPanel;
        this.currentPanel = new JPanel();
        this.currentPanel.setLayout(new BoxLayout(this.currentPanel,
            BoxLayout.Y_AXIS));
        processChildElements(e);
        previousPanel.add(this.currentPanel);
        this.currentPanel = previousPanel;
    }

    /**
     * From xscreensaver/hacks/config/README:
     *
     *  <_description>
     *        FREE TEXT
     *  </_description>
     *
     *        This is the description of the hack that appears in the right
     *        part of the window.  Lines are wrapped; paragraphs are separated
     *        by blank lines.  Lines that begin with whitespace will not be
     *        wrapped (see "munch.xml" for an example of why.)
     */
    private void processDescriptionElement(Element e) {
        StringBuffer description = new StringBuffer();
        String body = elementBody(e);
        StringTokenizer st = new StringTokenizer(body, "\n", true);
        boolean lastWasNewline = false, appended = false;
        // Double newline will cause new paragraph.  All other paragraphs
        // are merged to get cleaner wrapping.
        while(st.hasMoreTokens()) {
            String line = st.nextToken();
            if(line.equals("\n")) {
                if(lastWasNewline && !appended) {
                    description.append("\n\n");
                    appended = true;
                }
                lastWasNewline = true;
            }
            else {
                lastWasNewline = false;
                appended = false;
                description.append(line);
                if(Character.isWhitespace(line.charAt(0))) {
                    // do not wrap
                    description.append('\n');
                }
                else {
                    // wrap
                    description.append(' ');
                }
            }
        }
        taDocumentation.setText(description.toString());
    }
    
    /**
     * From xscreensaver/hacks/config/README:
     *
     *  <command arg="-SWITCH"/>
     *
     *        specifies that "-SWITCH" always appears on the command line.
     *        You'll most often see this with "-root".
     */
    private void processCommandElement(Element e) {
        // Always echo command, no control associated
        String arg = e.getAttribute("arg");
        commandline.add(arg);
    }
    
    /**
     * From xscreensaver/hacks/config/README:
     *
     *  <boolean id="SYMBOLIC NAME"
     *           _label="USER VISIBLE STRING"
     *            arg-set="-SWITCH-A"
     *            arg-unset="-SWITCH-B"
     *           />
     *
     *        This creates a checkbox.
     *
     *        "id" is currently unused, but may eventually be used for
     *        letting other widgets refer to this one.
     *
     *        "_label" is the string printed next to the checkbox.
     *
     *        "arg-set" is what to insert into the command line if the
     *        box is checked.
     *
     *        "arg-unset" is what to insert into the command line if the
     *        box is unchecked.
     *
     *        You will probably never specify both "arg-set" and "arg-unset",
     *        because the setting that is the default should insert nothing
     *        into the command line (that's what makes it the default.)
     *        For example:
     *
     *           <boolean "foo" arg_set="-foo">
     *
     *        or if "foo" is the default, and must be explicity turned off,
     *
     *           <boolean "foo" arg_unset="-no-foo">
     */
    private void processBooleanElement(Element e) {
        String label = e.getAttribute("_label");
        if(label==null) {
            label = "";
        }
        final String argSet = e.getAttribute("arg-set");
        final String argUnset = e.getAttribute("arg-unset");
        final JCheckBox checkbox = new JCheckBox(label);
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout());
        panel.add(checkbox);
        currentPanel.add(panel);
        
        // Check current setting
        if(argSet == null) {
            if(loadedSettingsContainsArg(argUnset)) {
                checkbox.setSelected(false);
                commandline.add(argUnset);
            }
            else {
                checkbox.setSelected(true);
                commandline.add("");
            }
        }
        else {
            if(loadedSettingsContainsArg(argSet)) {
                checkbox.setSelected(true);
                commandline.add(argSet);
            }
            else {
                checkbox.setSelected(false);
                commandline.add("");
            }
        }
        final int commandlineIndex = commandline.size() - 1;
        
        checkbox.addActionListener(
            new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    if(checkbox.isSelected()) {
                        commandline.set(commandlineIndex, 
                            (argSet == null) ? "" : argSet);
                    }
                    else {
                        commandline.set(commandlineIndex, 
                            (argUnset == null) ? "" : argUnset);
                    }
                    echoCommandline();
                }
            });
    }

    /**
     * Number elements can be either sliders or spin buttons.
     */
    private void processNumberElement(Element e) {
        String type = e.getAttribute("type");
        if(type.equals("slider")) {
            processNumberElementSlider(e);
        }
        else if(type.equals("spinbutton")) {
            processNumberElementSpinButton(e);
        }
    }
    
    /**
     * From xscreensaver/hacks/config/README:
     *
     *  <number id="SYMBOLIC NAME"
     *          type="slider"
     *          arg="-SWITCH %"
     *          _label="HEADING LABEL"
     *          _low-label="LEFT LABEL"
     *          _high-label="RIGHT LABEL"
     *          low="MIN VALUE"
     *          high="MAX VALUE"
     *          default="DEFAULT VALUE"
     *          [ convert="invert" ]
     *          />
     *
     *        This creates a slider.
     *
     *        The _label is printed above the slider.  The _low-label and
     *        _high-label are printed to the left and right, respectively.
     *
     *        If any of the numbers you type has a decimal point, then
     *        the range is assumed to be a floating-point value; otherwise,
     *        only integral values will be used.  So be careful about "1"
     *        versus "1.0".
     *
     *        If convert="invert" is specified, then the value that the
     *        user tweaks goes the other way from the value the command
     *        line expects: e.g., if the slider goes from 10-20 and the
     *        user picks 13, the converted value goes from 20-10 (and
     *        would be 17.)  This is useful for converting between the
     *        concepts of "delay" and "speed".
     *
     *        In the "arg" string, the first occurence of "%" is replaced
     *        with the numeric value, when creating the command line.
     *
     */
    private void processNumberElementSlider(Element e) {
        try {
            final int SLIDER_MAX = 1000;
            final String arg = e.getAttribute("arg");
            String label = e.getAttribute("_label");
            String lowLabel = e.getAttribute("_low-label");
            String highLabel = e.getAttribute("_high-label");
            String low = e.getAttribute("low");
            String high = e.getAttribute("high");
            String def = e.getAttribute("default");
            String current = def;
            boolean invert = "invert".equals(e.getAttribute("convert"));
            final boolean useFloat = 
                (low.indexOf('.') != -1) ||
                (high.indexOf('.') != -1) ||
                (def.indexOf('.') != -1);
            final Number nLow = useFloat ? 
                (Number)new Float(Float.parseFloat(low)) :
                (Number)new Integer(Integer.parseInt(low));
            final Number nHigh = useFloat ?
                (Number)new Float(Float.parseFloat(high)) :
                (Number)new Integer(Integer.parseInt(high));
            final Number nDefault = useFloat ?
                (Number)new Float(Float.parseFloat(def)) :
                (Number)new Integer(Integer.parseInt(def));
            
            GridBagConstraints gbc = new GridBagConstraints();
            JPanel panel = new JPanel();
            panel.setLayout(new GridBagLayout());
            
            // Label on top:
            if(label != null) {
                JLabel lbl = new JLabel(label);
                gbc.gridwidth = 1;
                gbc.anchor = GridBagConstraints.CENTER;
                panel.add(new JLabel(""), gbc);
                panel.add(lbl, gbc);
                gbc.gridwidth = GridBagConstraints.REMAINDER;
                panel.add(new JLabel(""), gbc);
            }
            
            // Label on left:
            gbc.gridwidth = 1;
            gbc.insets = new Insets(0, 2, 5, 2);
            if(lowLabel != null) {
                JLabel lbl = new JLabel(lowLabel);
                gbc.anchor = GridBagConstraints.EAST;
                panel.add(lbl, gbc);
            }
            
            // Slider
            // Determine current value.
            String currentValue = currentValueOfArg(arg);
            
            // We use 0-SLIDER_MAX because JSlider
            // does not support float min and max.
            int val;
            if(useFloat) {
                float floatDefault = nDefault.floatValue();
                try {
                    if(currentValue != null) {
                        floatDefault = Float.parseFloat(currentValue);
                        current = currentValue;
                    }
                }
                catch(NumberFormatException ex) {
                    // use existing default
                }
                float floatLow = nLow.floatValue();
                float floatHigh = nHigh.floatValue();
                val = (int)(((floatDefault - floatLow) * SLIDER_MAX) / 
                    (floatHigh - floatLow));
            }
            else {
                int intDefault = nDefault.intValue();
                try {
                    if(currentValue != null) {
                        intDefault = Integer.parseInt(currentValue);
                        current = currentValue;
                    }
                }
                catch(NumberFormatException ex) {
                    // use existing default
                }
                int intLow = nLow.intValue();
                int intHigh = nHigh.intValue();
                val = ((intDefault - intLow) * SLIDER_MAX) / 
                    (intHigh - intLow);
            }
            if(val < 0) val = 0;
            if(val > SLIDER_MAX) val = SLIDER_MAX;
            final JSlider slider = new JSlider(JSlider.HORIZONTAL, 0, 
                SLIDER_MAX, val);
            slider.setInverted(invert);
            // The full size is often too big and the right label gets cut off
            Dimension dim = new Dimension(
                slider.getPreferredSize().width * 1 / 2, 
                slider.getPreferredSize().height);
            slider.setPreferredSize(dim);
            gbc.anchor = GridBagConstraints.CENTER;
            gbc.fill = GridBagConstraints.HORIZONTAL;
            gbc.weightx = 1.0;
            if(highLabel == null) {
                gbc.gridwidth = GridBagConstraints.REMAINDER;
            }
            else {
                gbc.gridwidth = 1;
            }
            panel.add(slider, gbc);
            gbc.fill = GridBagConstraints.NONE;
            gbc.weightx = 0.0;
            
            // Label on right:
            if(highLabel != null) {
                JLabel lbl = new JLabel(highLabel);
                gbc.anchor = GridBagConstraints.WEST;
                gbc.gridwidth = GridBagConstraints.REMAINDER;
                panel.add(lbl, gbc);
            }
            
            commandline.add(replacePercent(arg, current));
            final int commandlineIndex = commandline.size() - 1;
            
            // Add panel with slider, etc. to box layout
            currentPanel.add(panel);

            // Listener for control:
            slider.addChangeListener(
                new ChangeListener() {
                    public void stateChanged(ChangeEvent e) {
                        int val = slider.getValue();
                        String value;
                        if(useFloat) {
                            float floatLow = nLow.floatValue();
                            float floatHigh = nHigh.floatValue();
                            value = "" + (floatLow + 
                                ((val * (floatHigh - floatLow)) / SLIDER_MAX));
                        }
                        else {
                            int intLow = nLow.intValue();
                            int intHigh = nHigh.intValue();
                            value = "" + (intLow +
                                ((val * (intHigh - intLow)) / SLIDER_MAX));
                        }
                        
                        commandline.set(commandlineIndex, 
                            replacePercent(arg, value));
                        echoCommandline();
                    }
                });
        }
        catch(NumberFormatException ex) {
            // ignore this invalid entry.
            ex.printStackTrace();
        }
    }
    
    /**
     * From xscreensaver/hacks/config/README:
     *
     *  <number id="SYMBOLIC NAME"
     *          type="spinbutton"
     *          arg="-SWITCH %"
     *          _label="HEADING LABEL"
     *          low="MIN VALUE"
     *          high="MAX VALUE"
     *          default="DEFAULT VALUE"
     *          [ convert="invert" ]
     *          />
     *
     *        This creates a spinbox (a text field with a number in it,
     *        and up/down arrows next to it.)
     *
     *        Arguments are exactly like type="slider", except that
     *        _low-label and _high-label are not used.  Also, _label
     *        appears to the left of the box, instead of above it.
     */
    private void processNumberElementSpinButton(Element e) {
        try {
            final String arg = e.getAttribute("arg");
            String label = e.getAttribute("_label");
            String low = e.getAttribute("low");
            String high = e.getAttribute("high");
            String def = e.getAttribute("default");
            String current = def;
            // XXX - invert is ignored for now
            boolean invert = "invert".equals(e.getAttribute("convert"));
            final boolean useFloat = 
                (low.indexOf('.') != -1) ||
                (high.indexOf('.') != -1) ||
                (def.indexOf('.') != -1);
            final Number nLow = useFloat ? 
                (Number)new Float(Float.parseFloat(low)) :
                (Number)new Integer(Integer.parseInt(low));
            final Number nHigh = useFloat ?
                (Number)new Float(Float.parseFloat(high)) :
                (Number)new Integer(Integer.parseInt(high));
            final Number nDefault = useFloat ?
                (Number)new Float(Float.parseFloat(def)) :
                (Number)new Integer(Integer.parseInt(def));
            
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.insets = new Insets(0, 2, 5, 2);
            JPanel panel = new JPanel();
            panel.setLayout(new GridBagLayout());
            
            // Label on left:
            if(label != null) {
                JLabel lbl = new JLabel(label);
                gbc.gridwidth = 1;
                gbc.anchor = GridBagConstraints.EAST;
                panel.add(lbl, gbc);
            }
            
            // Spinner control:
            // Determine current value.
            String currentValue = currentValueOfArg(arg);
            
            gbc.gridwidth = GridBagConstraints.REMAINDER;
            gbc.anchor = GridBagConstraints.WEST;
            gbc.weightx = 1.0;
            final JSpinner spinner = new JSpinner();
            SpinnerNumberModel spinnerModel;
            if(useFloat) {
                float floatDefault = nDefault.floatValue();
                try {
                    if(currentValue != null) {
                        floatDefault = Float.parseFloat(currentValue);
                        current = currentValue;
                    }
                }
                catch(NumberFormatException ex) {
                    // use existing default
                }
                float floatLow = nLow.floatValue();
                float floatHigh = nHigh.floatValue();
                spinnerModel = new SpinnerNumberModel(floatDefault, floatLow, 
                    floatHigh, 1.0);
            }
            else {
                int intDefault = nDefault.intValue();
                try {
                    if(currentValue != null) {
                        intDefault = Integer.parseInt(currentValue);
                        current = currentValue;
                    }
                }
                catch(NumberFormatException ex) {
                    // use existing default
                }
                int intLow = nLow.intValue();
                int intHigh = nHigh.intValue();
                spinnerModel = new SpinnerNumberModel(intDefault, intLow, 
                    intHigh, 1);
            }
            spinner.setModel(spinnerModel);
            panel.add(spinner, gbc);
            
            // Add spin button control with label to box layout
            currentPanel.add(panel);
            
            commandline.add(replacePercent(arg, current));
            final int commandlineIndex = commandline.size() - 1;
            
            // Listener for control:
            spinner.addChangeListener(
                new ChangeListener() {
                    public void stateChanged(ChangeEvent e) {
                        commandline.set(commandlineIndex, 
                            replacePercent(arg, 
                            spinner.getValue().toString()));
                        echoCommandline();
                    }
                });
        }
        catch(NumberFormatException ex) {
            // ignore this invalid entry.
            ex.printStackTrace();
        }
    }
    
    /**
     * From xscreensaver/hacks/config/README:
     *
     *  <select id="SYMBOLIC NAME">
     *    <option id="SYMBOLIC NAME"
     *            _label="USER VISIBLE STRING"
     *            arg-set="-SWITCH"
     *            />
     *    [ ... more <options> ... ]
     *  </select>
     *
     *        This creates a selection popup menu.
     *
     *        Options should have _arg-set, and never _arg-unset.
     *
     *        One of the menu items (the default) should have no
     *        _arg-set.
     */
    private void processSelectElement(Element e) {
        DefaultComboBoxModel model = new DefaultComboBoxModel();
        final JComboBox combo = new JComboBox(model);
        NodeList options = e.getChildNodes();
        // Arguments are stored in parallel array
        final ArrayList arguments = new ArrayList();
        String defaultSelection = null;
        boolean explicitlySet = false;
        String currentSelection = "";
        for(int i = 0; i < options.getLength(); i++) {
            Node n = options.item(i);
            if(n instanceof Element) {
                Element o = (Element)n;
                if(o.getTagName().equals("option")) {
                    String label = o.getAttribute("_label");
                    String argSet = o.getAttribute("arg-set");
                    if(argSet == null) {
                        argSet = "";
                    }
                    arguments.add(argSet);
                    model.addElement(label);
                    if(loadedSettingsContainsArg(argSet)) {
                        model.setSelectedItem(label);
                        currentSelection = argSet;
                        explicitlySet = true;
                    }
                    if(argSet.equals("")) {
                        defaultSelection = label;
                    }
                }
            }
        }
        if((defaultSelection != null) && !explicitlySet) {
            // Only set default selection if we didn't detect the right
            // argument from the commandline.
            model.setSelectedItem(defaultSelection);
        }
        currentPanel.add(combo);
        
        commandline.add(currentSelection);
        final int commandlineIndex = commandline.size() - 1;
        
        // Listener for control
        combo.addActionListener(
            new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    String arg = (String)arguments.get(
                        combo.getSelectedIndex());
                    commandline.set(commandlineIndex, arg);
                    echoCommandline();
                }
            });
    }

    /**
     * From xscreensaver/hacks/config/README:
     *
     *      <string id="SYMBOLIC NAME"
     *           _label="USER VISIBLE STRING"
     *           arg="-SWITCH %"
     *           />
     *
     *        This creates a text entry field.  Options should be obvious.
     */
    private void processStringElement(Element e) {
        String label = e.getAttribute("_label");
        final String arg = e.getAttribute("arg");
        String currentValue = currentValueOfArg(arg);
        
        JPanel panel = new JPanel();
        panel.setLayout(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(0, 2, 5, 2);
        if(label != null) {
            JLabel lbl = new JLabel(label);
            gbc.anchor = GridBagConstraints.EAST;
            panel.add(lbl, gbc);
        }
        final JTextField tf = new JTextField(15);
        gbc.anchor = GridBagConstraints.WEST;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        panel.add(tf, gbc);
        
        // Add text and label to box layout:
        currentPanel.add(panel);
        
        if(currentValue != null) {
            tf.setText(currentValue);
        }
        else {
            currentValue = "";
        }
        
        if(currentValue.equals("")) {
            commandline.add("");
        }
        else {
            commandline.add(replacePercent(arg, currentValue));
        }
        final int commandlineIndex = commandline.size() - 1;
        
        tf.getDocument().addDocumentListener(
            new DocumentListener() {
                public void changedUpdate(DocumentEvent e) {
                    update();
                }
                public void insertUpdate(DocumentEvent e) {
                    update();
                }
                public void removeUpdate(DocumentEvent e) {
                    update();
                }
                private void update() {
                    String text = tf.getText();
                    if(!text.equals("")) {
                        commandline.set(commandlineIndex, 
                            replacePercent(arg, text));
                    }
                    else {
                        commandline.set(commandlineIndex, "");
                    }
                    echoCommandline();
                }
            });
    }
    
    /**
     * From xscreensaver/hacks/config/README:
     *
     *  <file id="SYMBOLIC NAME"
     *        _label="USER VISIBLE STRING"
     *        arg="-SWITCH %"
     *        />
     *
     *        This creates a file entry field (a text field with a "Browse"
     *        button next to it.)
     */
    private void processFileElement(Element e) {
        String id = e.getAttribute("id");
        if("jdkhome".equals(id)) {
            // Do not provide any way to change jdk home, since this
            // does not yet work on Windows (on Windows, this is automatically
            // detected from the registry).  On Unix, the SettingsDialog
            // is never executed anyway.
            return;
        }
        String label = e.getAttribute("_label");
        final String arg = e.getAttribute("arg");
        String currentValue = currentValueOfArg(arg);
        GridBagConstraints gbc = new GridBagConstraints();
        JPanel panel = new JPanel();
        panel.setLayout(new GridBagLayout());
        gbc.insets = new Insets(0, 2, 5, 2);
        if(label != null) {
            JLabel lbl = new JLabel(label);
            gbc.anchor = GridBagConstraints.EAST;
            panel.add(lbl, gbc);
        }
        gbc.anchor = GridBagConstraints.WEST;
        gbc.weightx = 1.0;
        final JButton browse = new JButton("Browse...");
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        panel.add(browse, gbc);
        
        // Add chooser to box layout:
        currentPanel.add(panel);
        
        if(currentValue != null) {
            browse.setText(shortenString(currentValue, 15));
        }
        else {
            currentValue = "";
        }
        
        final StringBuffer path = new StringBuffer(currentValue);
        if(currentValue.equals("")) {
            commandline.add("");
        }
        else {
            commandline.add(replacePercent(arg, currentValue));
        }
        final int commandlineIndex = commandline.size() - 1;
        
        browse.addActionListener(
            new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    if(chooser == null) {
                        chooser = new JFileChooser();
                    }
                    chooser.setSelectedFile(new File(path.toString()));
                    chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
                    if(chooser.showOpenDialog(SettingsDialog.this) == 
                        JFileChooser.APPROVE_OPTION)
                    {
                        path.replace(0, path.length(), 
                            chooser.getSelectedFile().getAbsolutePath());
                        browse.setText(shortenString(path.toString(),15));
                        if(path.length() > 0) {
                            commandline.set(commandlineIndex, 
                                replacePercent(arg, path.toString()));
                        }
                        else {
                            commandline.set(commandlineIndex, "");
                        }
                    }
                    else {
                        browse.setText("Browse...");
                        path.replace(0, path.length(), "");
                        commandline.set(commandlineIndex, "");
                    }
                    echoCommandline();
                }
            });
    }
    
    /**
     * Echoes the current commandline
     */
    private void echoCommandline() {
        System.out.println(getNormalizedCommandline());
    }
    
    /**
     * Returns the normalized command line
     */
    private String getNormalizedCommandline() {
        StringBuffer result = new StringBuffer();
        for(int i = 0; i < commandline.size(); i++) {
            String arg = ((String)commandline.get(i)).trim();
            result.append(arg);
            if(!arg.equals("")) {
                result.append(" ");
            }
        }
        return result.toString();
    }
    
    /**
     * Gets all the text nodes in the body of an element and concatenates them
     */
    private String elementBody(Element e) {
        StringBuffer result = new StringBuffer();
        NodeList list = e.getChildNodes();
        for(int i = 0; i < list.getLength(); i++) {
            Node n = list.item(i);
            result.append(n.getNodeValue());
        }
        return result.toString();
    }
    
    /**
     * Replaces the first occurence of % with the given String
     * Also adds quotes around a value with spaces.
     */
    private String replacePercent(String line, String valueParam) {
        String result;
        String value = valueParam;
        if(value.indexOf(" ") != -1) {
            value = "\"" + value + "\"";
        }

        int index = line.indexOf('%');
        if(index == -1) {
            result = line;
        }
        else {
            result = line.substring(0, index) + value + 
                line.substring(index + 1);
        }
        return result;
    }
    
    /**
     * Searches the commandline of the loaded settings to see if the given
     * argument is present.
     */
    private boolean loadedSettingsContainsArg(String arg) {
        String commandline = settings.getNormalizedCommandline();
        commandline = " " + commandline + " ";
        // add spaces before and after so as not to include substrings like
        // -me matching -mem
        return commandline.indexOf(" " + arg.trim() + " ") != -1;
    }
    
    /**
     * Searches the commandline of the loaded settings for the current value of
     * the given argument.  Returns null if the value could not be found.
     */
    private String currentValueOfArg(String arg) {
        String result = null;
        if(arg.startsWith("-") && (arg.indexOf(' ') != -1)) {
            String key = arg.substring(1,arg.indexOf(' '));
            result = settings.getProperty(key);
        }
        return result;
    }
    
    /**
     * Shortens the given string to the given # of characters.
     */
    private String shortenString(String path, int len) {
        String result = path;
        if(path.length() > len) {
            result = "..." + path.substring(
                path.length() - len - 3, path.length());
        }
        return result;
    }
}
