Index: src/java/org/xhtmlrenderer/simple/extend/form/SelectField.java
===================================================================
RCS file: /cvs/xhtmlrenderer/src/java/org/xhtmlrenderer/simple/extend/form/SelectField.java,v
retrieving revision 1.4
diff -u -r1.4 SelectField.java
--- src/java/org/xhtmlrenderer/simple/extend/form/SelectField.java	25 Apr 2009 11:50:34 -0000	1.4
+++ src/java/org/xhtmlrenderer/simple/extend/form/SelectField.java	28 Jul 2009 15:38:15 -0000
@@ -19,14 +19,22 @@
  */
 package org.xhtmlrenderer.simple.extend.form;
 
+import java.awt.Component;
+import java.awt.Font;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.swing.DefaultListCellRenderer;
 import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JList;
 import javax.swing.JScrollPane;
+import javax.swing.ListModel;
 import javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
 
 import org.w3c.dom.Element;
 import org.w3c.dom.NodeList;
@@ -44,6 +52,9 @@
         // Either a select list or a drop down/combobox
         if (shouldRenderAsList()) {
             JList select = new JList(optionList.toArray());
+            
+            select.setCellRenderer(new CellRenderer());
+            select.addListSelectionListener(new HeadingItemListener());
 
             if (hasAttribute("multiple") && getAttribute("multiple").equalsIgnoreCase("true")) {
                 select.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
@@ -69,6 +80,8 @@
             JComboBox select = new JComboBox(optionList.toArray());
 
             select.setEditable(false);
+            select.setRenderer(new CellRenderer());
+            select.addItemListener(new HeadingItemListener());
 
             return select;
         }
@@ -120,7 +133,9 @@
             String [] submitValues = new String [selectedValues.length];
             
             for (int i = 0; i < selectedValues.length; i++) {
-                submitValues[i] = ((NameValuePair) selectedValues[i]).getValue();
+                NameValuePair pair = (NameValuePair) selectedValues[i];
+                if (pair.getValue()!=null)
+                    submitValues[i] = pair.getValue();
             }
             
             return submitValues;
@@ -129,33 +144,46 @@
             
             NameValuePair selectedValue = (NameValuePair) select.getSelectedItem();
             
-            if (selectedValue == null) {
-                return new String [] {};
-            } else {
-                return new String [] { selectedValue.getValue() };
+            if (selectedValue != null) {
+                if (selectedValue.getValue()!=null)
+                    return new String [] { selectedValue.getValue() };
             }
         }
+
+        return new String [] {};
     }
 
     private List createList() {
-        List list = new ArrayList();
-
-        NodeList options = getElement().getElementsByTagName("option");
+        List list = new ArrayList();        
+        addChildren(list, getElement(), 0);
+        return list;
+    }
 
-        for (int i = 0; i < options.getLength(); i++) {
-            Element option = (Element) options.item(i);
-            
-            String optionText = XhtmlForm.collectText(option);
-            String optionValue = optionText;
-            
-            if (option.hasAttribute("value")) {
-                optionValue = option.getAttribute("value");
+    private void addChildren(List list, Element e, int indent) {
+        NodeList children = e.getChildNodes();
+        
+        for (int i = 0; i < children.getLength(); i++) {
+            if (!(children.item(i) instanceof Element)) continue;
+            Element child = (Element) children.item(i);
+            
+            if ("option".equals(child.getNodeName())) {
+                // option tag, add it
+                String optionText = XhtmlForm.collectText(child);
+                String optionValue = optionText;
+                
+                if (child.hasAttribute("value")) {
+                    optionValue = child.getAttribute("value");
+                }
+
+                list.add(new NameValuePair(optionText, optionValue, indent));
+                
+            } else if ("optgroup".equals(child.getNodeName())) {
+                // optgroup tag, append heading and indent children
+                String titleText = child.getAttribute("label");
+                list.add(new NameValuePair(titleText, null, indent));
+                addChildren(list, child, indent+1);
             }
-
-            list.add(new NameValuePair(optionText, optionValue));
         }
-        
-        return list;
     }
     
     private boolean shouldRenderAsList() {
@@ -177,14 +205,22 @@
     /**
      * Provides a simple container for name/value data, such as that used
      * by the &lt;option&gt; elements in a &lt;select&gt; list.
+     * <p>
+     * When the value is {@code null}, this pair is used as a heading and
+     * should not be selected by itself.
+     * <p>
+     * The indent property was added to support indentation of items as
+     * children below headings.
      */
     private static class NameValuePair {
         private String _name;
         private String _value;
+        private int _indent;
 
-        public NameValuePair(String name, String value) {
+        public NameValuePair(String name, String value, int indent) {
             _name = name;
             _value = value;
+            _indent = indent;
         }
         
         public String getName() {
@@ -194,10 +230,100 @@
         public String getValue() {
             return _value;
         }
+        
+        public int getIndent() {
+            return _indent;
+        }
 
         public String toString() {
-            return getName();
+            String txt = getName();
+            for (int i = 0; i < getIndent(); i++)
+                txt = "    " + txt;
+            return txt;
         }
     }
+    
+    /**
+     * Renderer for ordinary items and headings in a List.
+     */
+    private static class CellRenderer extends DefaultListCellRenderer {
+        public Component getListCellRendererComponent(JList list, Object value,
+                int index, boolean isSelected, boolean cellHasFocus) {
+            NameValuePair pair = (NameValuePair)value;
+            
+            if (pair!=null && pair.getValue()==null) {
+                // render as heading as such
+                super.getListCellRendererComponent(list, value, index, false, false);
+                Font fold = getFont();
+                Font fnew = new Font(fold.getName(), Font.BOLD | Font.ITALIC, fold.getSize());
+                setFont(fnew);
+            } else {
+                // other items as usuall
+                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+            }
+            
+            return this;
+        }
+    }
+    
+    /**
+     * Helper class that makes headings inside a list unselectable
+     * <p>
+     * This is an {@linkplain ItemListener} for comboboxes, and a
+     * {@linkplain ListSelectionListener} for lists.
+     */
+    private static class HeadingItemListener implements ItemListener, ListSelectionListener {
+        
+        private Object oldSelection = null;
+        private int[] oldSelections = new int[0];
+        
+        public void itemStateChanged(ItemEvent e) {
+            if (e.getStateChange() != ItemEvent.SELECTED)
+                return;
+            // only for comboboxes
+            if (! (e.getSource() instanceof JComboBox) )
+                return;
+            JComboBox combo = (JComboBox)e.getSource();
+            
+            if (((NameValuePair)e.getItem()).getValue() == null) {
+                // header selected: revert to old selection
+                combo.setSelectedItem(oldSelection);
+            } else {
+                // store old selection
+                oldSelection = e.getItem();
+            }
+        }
 
+        public void valueChanged(ListSelectionEvent e) {
+            // only for lists
+            if (! (e.getSource() instanceof JList) )
+                return;
+            JList list = (JList)e.getSource();
+            ListModel model = list.getModel();
+            
+            // deselect all headings
+            for (int i = e.getFirstIndex(); i <= e.getLastIndex(); i++) {
+                if (!list.isSelectedIndex(i)) continue;
+                NameValuePair pair = (NameValuePair) model.getElementAt(i);
+                if ( pair!=null && pair.getValue() == null) {
+                    // We have a heading, remove it. As this handler is called
+                    // as a result of the resulting removal and we do process
+                    // the events while the value is adjusting, we don't need
+                    // to process any other headings here.
+                    // BUT if there'll be no selection anymore because by selecting
+                    // this one the old selection was cleared, restore the old
+                    // selection.
+                    if (list.getSelectedIndices().length==1)
+                        list.setSelectedIndices(oldSelections);
+                    else
+                        list.removeSelectionInterval(i, i);
+                    return;
+                }
+            }
+            
+            // if final selection: store it
+            if (!e.getValueIsAdjusting())
+                oldSelections = list.getSelectedIndices();
+        }
+    }
 }
