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 <option> elements in a <select> list. + *

+ * When the value is {@code null}, this pair is used as a heading and + * should not be selected by itself. + *

+ * 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 + *

+ * 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(); + } + } }