package net.sf.egonet.web.panel; import java.io.Serializable; import java.util.List; import java.util.ArrayList; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import net.sf.egonet.web.component.FocusOnLoadBehavior; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.Component; import org.apache.wicket.behavior.SimpleAttributeModifier; import org.apache.wicket.model.Model; import com.google.common.collect.Lists; public class CheckboxesPanel<T> extends Panel { private final String dontKnow = AnswerFormFieldPanel.dontKnow, refuse = AnswerFormFieldPanel.refuse, none = AnswerFormFieldPanel.none; protected String showItem(T item) { String strItem = item.toString(); return( strItem); } protected String hotkey(Object obj) { return null; } private static final String otherSpecify = "OTHER SPECIFY"; private List<CheckableWrapper> items; private List<AjaxCheckBox> checkBoxes; private Boolean otherSpecifyStyle; private Boolean otherSelected; private ArrayList<ActionListener> actionListeners; private ArrayList<Component> componentsToUpdate; private Form horizontalForm; private Form verticalForm; private boolean horizontalLayout = false; private int maxStringLength = 100; public List<T> getSelected() { List<T> result = Lists.newArrayList(); for(CheckableWrapper wrapper : items) { if(wrapper.getSelected()) { result.add(wrapper.getItem()); } } return result; } public CheckboxesPanel(String id, List<T> items, List<T> selected) { super(id); actionListeners = new ArrayList<ActionListener>(); componentsToUpdate = new ArrayList<Component>(); otherSpecifyStyle = false; otherSelected = false; if ( items.size()>20 ) { setMaxStringLength(12); } else { switch ( items.size()) { case 19: setMaxStringLength(13); break; case 18: setMaxStringLength(13); break; case 17: setMaxStringLength(14); break; case 16: setMaxStringLength(15); break; case 15: setMaxStringLength(16); break; case 14: setMaxStringLength(17); break; case 13: setMaxStringLength(18); break; case 12: setMaxStringLength(20); break; case 11: setMaxStringLength(22); break; default: setMaxStringLength(100); break; } } this.items = Lists.newArrayList(); for(T item : items) { this.items.add( new CheckableWrapper(item) .setSelected(selected.contains(item))); } build(); } private Boolean autoFocus = false; /** * this will be used in horizontal layouts in the list-of-alters * page to get more items on each line * @param str the original checkbox label text * @return a (possibly shortened) label */ private String truncate(String str) { if ( str.length() > maxStringLength ) str = str.substring(0,maxStringLength); return(str); } /** * the horizontal form of the checkboxes will depend on javascript * functions being available in the file listofalters.js */ private void build() { horizontalForm = new Form ("horizontalForm"); add(horizontalForm); checkBoxes = Lists.newArrayList(); ListView checkboxes = new ListView("checkboxes",items) { protected void populateItem(ListItem item) { CheckableWrapper wrapper = (CheckableWrapper) item.getModelObject(); item.add(new Label("checkLabel", truncate(wrapper.getName()))); AjaxCheckBox checkBox = new AjaxCheckBox("checkField", new PropertyModel(wrapper, "selected")) { protected void onUpdate(AjaxRequestTarget target) { boolean otherNowSelected = false; otherNowSelected = isOtherSelected(); if ( otherNowSelected != otherSelected ) { for ( Component component : componentsToUpdate) { target.addComponent(component); } fireActionEvent (otherNowSelected, otherSpecify ); otherSelected = otherNowSelected; } }}; checkBox.add( new SimpleAttributeModifier("onfocus","doOnFocusHorz(this);")); checkBox.add( new SimpleAttributeModifier("onblur", "doOnBlur(this);")); checkBox.add( new SimpleAttributeModifier("onkeyup","doOnKeyUpHorz(event);")); checkBoxes.add(checkBox); if(autoFocus) { if(wrapper.getName() != null && items.get(0).getName() != null && wrapper.getName().equals(items.get(0).getName())) { checkBox.add(new FocusOnLoadBehavior()); } } item.add(checkBox); } }; checkboxes.setReuseItems(true); horizontalForm.add(checkboxes); add(horizontalForm); verticalForm = new Form ("verticalForm"); add(verticalForm); ListView checkboxesVertical = new ListView("checkboxesVertical",items) { protected void populateItem(ListItem item) { CheckableWrapper wrapper = (CheckableWrapper) item.getModelObject(); Label label = new Label("checkLabelVertical", wrapper.getName()); AjaxCheckBox checkBox = new AjaxCheckBox("checkFieldVertical", new PropertyModel(wrapper, "selected")) { protected void onUpdate(AjaxRequestTarget target) { boolean otherNowSelected = false; otherNowSelected = isOtherSelected(); if ( otherNowSelected != otherSelected ) { for ( Component component : componentsToUpdate) { target.addComponent(component); } fireActionEvent (otherNowSelected, otherSpecify ); otherSelected = otherNowSelected; } }}; // vertical lists of less than 10 items will have numeric hotkeys if (items.size() >= 10 ) { checkBox.add( new SimpleAttributeModifier("onfocus","doOnFocusVert(this);")); checkBox.add( new SimpleAttributeModifier("onblur", "doOnBlur(this);")); checkBox.add( new SimpleAttributeModifier("onkeyup","doOnKeyUpVert(event);")); } else { checkBox.add( new SimpleAttributeModifier("onfocus","addHilite(this);")); checkBox.add( new SimpleAttributeModifier("onblur", "remHilite(this);")); } checkBoxes.add(checkBox); // this was an attempt to remove the 'hotkey' class from // Don't know and Refuse checkboxes. // it didn't work - the hotkey action was removed, but // the number in parenthesis remained in the label // if ( wrapper.getName().equals(AnswerFormFieldPanel.dontKnow) || // wrapper.getName().equals(AnswerFormFieldPanel.refuse)) { // checkBox.add( new SimpleAttributeModifier("class","")); // } if(autoFocus) { if(wrapper.getName() != null && items.get(0).getName() != null && wrapper.getName().equals(items.get(0).getName())) { checkBox.add(new FocusOnLoadBehavior()); } } String hk = hotkey(wrapper.getItem()); if(hk != null) { label.add(new SimpleAttributeModifier("hotkey",hk)); checkBox.add(new SimpleAttributeModifier("hotkey",hk)); } item.add(label); item.add(checkBox); } }; checkboxesVertical.setReuseItems(true); verticalForm.add(checkboxesVertical); add(verticalForm); if ( horizontalLayout ) horizontalForm.setVisible(true); else verticalForm.setVisible(false); } public void setHorizontalLayout ( boolean horizontalLayout ) { this.horizontalLayout = horizontalLayout; horizontalForm.setVisible(horizontalLayout); verticalForm.setVisible(!horizontalLayout); } public boolean getHorizontalLayout() { return(horizontalLayout); } public class CheckableWrapper implements Serializable { private T item; private Boolean selected; public CheckableWrapper(T item) { this.item = item; this.selected = false; } public CheckableWrapper setSelected(Boolean selected) { this.selected = selected; return this; } public Boolean getSelected() { return selected; } public T getItem() { return item; } public String getName() { return showItem(item); } } public void setAutoFocus() { this.autoFocus = true; } /** * the 'otherSpecifyStyle' indicates if we want a textfield to * appear when 'Other' is selected, If this is trur * we will want to send actionEvents to the listeners * @param otherSpecifyStyle */ public void setOtherSpecifyStyle(Boolean otherSpecifyStyle) { this.otherSpecifyStyle = (( otherSpecifyStyle==null)? false : otherSpecifyStyle ); } public Boolean getOtherSpecifyStyle() { return(otherSpecifyStyle); } /** * adds an ActionListener to the list of objects * that want to be notified of events. * In practice, the MultipleSelectionAnswerFormFieldPanel * is the only outside object interested, and then only * on whether the 'other' button is checked or unchecked, * and even that is ONLY if the question has the otherSpecifyType * flag set. This and the following functions are here to * maintain loose binding * @param aListener ActionListener to add */ public void addActionListener(ActionListener aListener) { if ( !actionListeners.contains(aListener)) actionListeners.add(aListener); } /** * simply removes an ActionListener from the list of * ActionListeners * @param aListener ActionListener to remove */ public void removeActionListener(ActionListener aListener) { actionListeners.remove(aListener); } /** * similar to the list of actionlisteners, * keep a list of components to update. * These will in reality only be 'Other/Specify' items * in a multiple selection question with an OTHER SPECIFY item * @param component */ public void addComponentToUpdate(Component component) { if ( !componentsToUpdate.contains(component)) componentsToUpdate.add(component); } /** * simply removes an component from the list of * componentsToUpdate * @param component Component to remove */ public void removeComponentToUpdate(Component component) { componentsToUpdate.remove(component); } /** * fires an actionEvent to all listeners * @param id 0 or 1 depending on whether a checkbox is selected * @param strMessage name of the checkbox. */ public void fireActionEvent(boolean selected, String strMessage) { ActionEvent ae ; for ( ActionListener aListener:actionListeners ) { ae = new ActionEvent(this, (selected?1:0), strMessage); aListener.actionPerformed(ae); } } /** * this checks the list of checkbox items, looking for one named 'Other' * and, if found, returns is selected status. * returns false if a checkbox names 'Other' is not located * @return selection status of 'Other' checkbox */ private boolean isOtherSelected() { if ( !otherSpecifyStyle ) return(false); for ( CheckableWrapper checkWrapper:items ) { if ( checkWrapper.getName().trim().startsWith(otherSpecify)) { return(checkWrapper.getSelected()); } } return(false); } public boolean getOtherSelected(boolean forceRecalcuation) { if ( forceRecalcuation ) otherSelected = isOtherSelected(); return(otherSelected); } public void setMaxStringLength(int maxStringLength) { this.maxStringLength = maxStringLength; } public int getMaxStringLength() { return(maxStringLength);} /** * used in those cases of a list-of-alters screen and the * user clicks one of the 'global' Don't know or Refuse buttons * if nothing is selected in this group of checkboxes, search * for one that matches the string and select it * @param selection - string to look for. generally 'Don't know' or 'Refuse' * @param iMaxSelection - maximum number of selections allowed in this * set of checkboxes * @return true if a new checkbox is selected */ public boolean forceSelectionIfNone(String selection, int iMaxSelection) { int iSelectionCount = 0; for ( CheckableWrapper checkWrapper : items ) { if ( checkWrapper.getSelected()) { ++iSelectionCount; if ( checkWrapper.getName().endsWith(dontKnow)) return(false); if ( checkWrapper.getName().endsWith(refuse)) return(false); } } // if we already have the maximum number of checkboxes checked, // exit if ( iSelectionCount >= iMaxSelection ) return(false); // if everything is unchecked, search for an item // that matches the string and select it // This goes through a rather round-about way of updating // the modelObject of the checkBox, but I found this was nessecary // to get the screen to update. for ( CheckableWrapper checkWrapper : items ) { if ( checkWrapper.getName().equalsIgnoreCase(selection)) { checkWrapper.setSelected(true); for ( AjaxCheckBox cBox : checkBoxes ) { if ( (Boolean)cBox.getModelObject()) cBox.setModelObject( new Model(Boolean.TRUE)); } return(true); } } return(false); } }