package org.openrosa.client.view;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import org.openrosa.client.model.DynamicOptionDef;
import org.openrosa.client.model.FormDef;
import org.openrosa.client.model.OptionDef;
import org.openrosa.client.model.QuestionDef;
import org.openrosa.client.util.FormDesignerUtil;
import org.openrosa.client.widget.skiprule.FieldWidget;
import org.openrosa.client.Context;
import org.openrosa.client.view.FormDesignerWidget;
import org.openrosa.client.controller.ItemSelectionListener;
import org.openrosa.client.locale.LocaleText;
import org.openrosa.client.util.FormUtil;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.PushButton;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwt.xml.client.Node;
/**
* This widget enables creation of dynamic selection lists.
*
* @author daniel
*
*/
public class DynamicListsView extends Composite implements ItemSelectionListener, ClickHandler{
/** The main or root widget. */
private VerticalPanel verticalPanel = new VerticalPanel();
/** Widget to display the "Values for" text. */
private Label lblValuesFor = new Label(LocaleText.get("valuesFor"));
/** Widget to display the is equal to text. */
private Label lblEqual = new Label(" "+LocaleText.get("isEqualTo"));
/** The widget for selection of the parent questions.
* The parent question is the one on which the single select dynamic question depends.
*/
private FieldWidget fieldWidget;
/** Widget to display the list of parent question options to select from. */
private ListBox lbOption = new ListBox(false);
/** Table to hold the list of dynamic options. */
private FlexTable table = new FlexTable();
/** Button to add a new dynamic selection list option. */
private Button btnAdd = new Button(LocaleText.get("addNew"));
/** The form definition object that this dynamic list belongs to. */
private FormDef formDef;
/** The question that we are building this dynamic selection list for.
* For the Continent and Country questions, this would be the Country question.
*/
private QuestionDef questionDef;
/** Flag determining whether to enable this widget or not. */
private boolean enabled;
/** The dynamic option definition object that we are building. */
private DynamicOptionDef dynamicOptionDef;
/** Contains the list of child options for the parent question's selected option. */
private List<OptionDef> optionList;
/** The parent question whose selected option determines the list of child options.
* For the Continent and Country questions, this would be the Continent question.
*/
private QuestionDef parentQuestionDef;
/**
* Creates a new instance of the dynamic lists widget.
*/
public DynamicListsView(){
setupWidgets();
}
/**
* Sets up widgets.
*/
private void setupWidgets(){
fieldWidget = new FieldWidget(this);
fieldWidget.setForDynamicOptions(true);
HorizontalPanel horizontalPanel = new HorizontalPanel();
horizontalPanel.add(lblValuesFor);
horizontalPanel.add(fieldWidget);
horizontalPanel.add(lblEqual);
horizontalPanel.add(lbOption);
horizontalPanel.setSpacing(5);
verticalPanel.add(horizontalPanel);
verticalPanel.add(table);
lbOption.addChangeHandler(new ChangeHandler(){
public void onChange(ChangeEvent event){
updateOptionList();
}
});
btnAdd.addClickHandler(this);
table.setStyleName("cw-FlexTable");
table.setWidget(0, 0,new Label(LocaleText.get("text")));
table.setWidget(0, 1,new Label(LocaleText.get("binding")));
table.setWidget(0, 2,new Label(LocaleText.get("action")));
table.getFlexCellFormatter().setColSpan(0, 2, 3);
table.setWidth("100%");
table.setHeight("100%");
table.getCellFormatter().setStyleName(0, 0, "getting-started-label");
table.getCellFormatter().setStyleName(0, 1, "getting-started-label");
table.getCellFormatter().setStyleName(0, 2, "getting-started-label");
initWidget(verticalPanel);
}
/**
* Sets the dynamic selection list question, whose list of options we set on this widget.
*
* @param questionDef the question.
*/
public void setQuestionDef(QuestionDef questionDef){
if(questionDef == null || questionDef.getDataType() != QuestionDef.QTN_TYPE_LIST_EXCLUSIVE_DYNAMIC){
setEnabled(false);
return;
}
setEnabled(true);
clear();
parentQuestionDef = null;
optionList = null;
dynamicOptionDef = null;
//TODO ???
formDef = questionDef.getParentFormDef();
/*if(questionDef.getParent() instanceof PageDef)
formDef = ((PageDef)questionDef.getParent()).getParent();
else
formDef = ((PageDef)((QuestionDef)questionDef.getParent()).getParent()).getParent();*/
if(questionDef != null)
lblValuesFor.setText(LocaleText.get("valuesFor") + questionDef.getDisplayText() + " "+LocaleText.get("whenAnswerFor"));
else
lblValuesFor.setText(LocaleText.get("valuesFor"));
this.questionDef = questionDef;
fieldWidget.setDynamicQuestionDef(questionDef);
fieldWidget.setFormDef(formDef);
QuestionDef parentQuestionDef = formDef.getDynamicOptionsParent(questionDef.getId());
if(parentQuestionDef != null)
fieldWidget.selectQuestion(parentQuestionDef);
}
/**
* Sets the form definition object that this dynamic selection list belongs to.
*
* @param formDef the form definition object.
*/
public void setFormDef(FormDef formDef){
updateDynamicLists();
this.formDef = formDef;
questionDef = null;
parentQuestionDef = null;
optionList = null;
dynamicOptionDef = null;
clear();
}
/**
* Removes all dynamic selection list values for any previous widget, if any.
*/
private void clear(){
if(questionDef != null)
updateDynamicLists();
questionDef = null;
lblValuesFor.setText(LocaleText.get("valuesFor"));
lbOption.clear();
while(verticalPanel.getWidgetCount() > 4)
verticalPanel.remove(verticalPanel.getWidget(3));
clearChildOptions();
fieldWidget.setQuestion(null);
}
/**
* Removes all options from the options list table.
*/
private void clearChildOptions(){
//Removes all options apart from the header which is at index 0.
while(table.getRowCount() > 1)
table.removeRow(1);
}
/**
* Sets whether to enable this widget or not.
*
* @param enabled set to true to enable, else false.
*/
public void setEnabled(boolean enabled){
this.enabled = enabled;
lbOption.setEnabled(enabled);
if(!enabled)
clear();
}
/**
* Checks whether this widget is enabled or not.
*
* @return true of enabled, else false.
*/
public boolean isEnabled(){
return enabled;
}
/**
* @see org.openrosa.client.controller.ItemSelectionListener#onItemSelected(Object, Object)
*/
public void onItemSelected(Object sender, Object item) {
//This is only useful for us when a new parent question has been selected.
if(sender != fieldWidget)
return;
//Clear all parent and child options.
lbOption.clear();
clearChildOptions();
parentQuestionDef = (QuestionDef)item;
//we only allow option lists for single select and single select dynamic types.
int type = parentQuestionDef.getDataType();
if(!(type == QuestionDef.QTN_TYPE_LIST_EXCLUSIVE || type == QuestionDef.QTN_TYPE_LIST_EXCLUSIVE_DYNAMIC))
return;
//Get the dynamic option definition object for which the selected
//question acts as the parent of the relationship.
dynamicOptionDef = formDef.getDynamicOptions(parentQuestionDef.getId());
//As for now, we do not allow the a parent question to map to more
//than once child question.
if(dynamicOptionDef != null && dynamicOptionDef.getQuestionId() != questionDef.getId())
return;
//Populate the list of parent options from a single select question.
if(type == QuestionDef.QTN_TYPE_LIST_EXCLUSIVE){
if(!(parentQuestionDef.getOptionCount() > 0)){
//we are creating new DynamicOptionDef() because we want to allow
//one specify type to be single select dynamic without specifying any
//options for cases where they will be got from the server using the
//external source widget filter property.
dynamicOptionDef = new DynamicOptionDef();
dynamicOptionDef.setQuestionId(questionDef.getId());
return;
}
List options = parentQuestionDef.getOptions();
for(int i=0; i<options.size(); i++){
OptionDef optionDef = (OptionDef)options.get(i);
lbOption.addItem(optionDef.getText(),String.valueOf(optionDef.getId()));
}
}
//Populate the list of parent options from a dynamic selection list question.
if(type == QuestionDef.QTN_TYPE_LIST_EXCLUSIVE_DYNAMIC){
if(dynamicOptionDef == null){
//we are creating new DynamicOptionDef() because we want to allow
//one specify type to be single select dynamic without specifying any
//options for cases where they will be got from the server using the
//external source widget filter property.
dynamicOptionDef = new DynamicOptionDef();
dynamicOptionDef.setQuestionId(questionDef.getId());
}
DynamicOptionDef options = formDef.getChildDynamicOptions(parentQuestionDef.getId());
if(options != null && options.getParentToChildOptions() != null){
Iterator<Entry<Integer,List<OptionDef>>> iterator = options.getParentToChildOptions().entrySet().iterator();
while(iterator.hasNext()){
Entry<Integer,List<OptionDef>> entry = iterator.next();
List<OptionDef> list = entry.getValue();
for(int index = 0; index < list.size(); index++){
OptionDef optionDef = list.get(index);
lbOption.addItem(optionDef.getText(),String.valueOf(optionDef.getId()));
}
}
}
}
//If there is any selection, update the table of options.
if(lbOption.getSelectedIndex() >= 0)
updateOptionList();
}
/**
* @see org.openrosa.client.controller.ItemSelectionListener#onStartItemSelection(Object)
*/
public void onStartItemSelection(Object sender){
}
/**
* Updates the form definition object with the dynamic option definition object
* that is being edited on this widget.
*/
public void updateDynamicLists(){
//dynamicOptionDef.size() == 0 is commented out because we want to allow
//one specify type to be single select dynamic without specifying any
//options for cases where they will be got from the server using the
//external source widget filter property.
if(dynamicOptionDef == null /*|| dynamicOptionDef.size() == 0*/){
if(parentQuestionDef != null)
formDef.removeDynamicOptions(parentQuestionDef.getId());
return;
}
formDef.setDynamicOptionDef(parentQuestionDef.getId(), dynamicOptionDef);
}
/**
* Populates the table of dynamic options with those options that are allowed
* for the currently selected option for the parent question.
*/
public void updateOptionList(){
clearChildOptions();
if(dynamicOptionDef == null){
dynamicOptionDef = new DynamicOptionDef();
dynamicOptionDef.setQuestionId(questionDef.getId());
}
int optionId = Integer.parseInt(lbOption.getValue(lbOption.getSelectedIndex()));
optionList = dynamicOptionDef.getOptionList(optionId);
if(optionList == null){
optionList = new ArrayList<OptionDef>();
dynamicOptionDef.setOptionList(optionId, optionList);
}
for(int index = 0; index < optionList.size(); index++){
OptionDef optionDef = optionList.get(index);
addOption(optionDef.getText(),optionDef.getQuestionID(),table.getRowCount());
}
addAddButton();
}
/**
* Called when any of the add new, delete, move up or move down
* button has been clicked.
*
* @sender the button which was clicked.
*/
public void onClick(ClickEvent event){
Object sender = event.getSource();
if(sender == btnAdd)
addNewOption().setFocus(true);
else{
int rowCount = table.getRowCount();
for(int row = 1; row < rowCount; row++){
//Delete button
if(sender == table.getWidget(row, 2)){
OptionDef optionDef = optionList.get(row-1);
if(!Window.confirm(LocaleText.get("removeRowPrompt") + " [" + optionDef.getText() + " - " + optionDef.getQuestionID() + "]"))
return;
table.removeRow(row);
optionList.remove(row-1);
if(optionDef.getControlNode() != null && optionDef.getControlNode().getParentNode() != null)
optionDef.getControlNode().getParentNode().removeChild(optionDef.getControlNode());
break;
}
else if(sender == table.getWidget(row, 3)){
//Move up button.
if(row == 1)
return;
moveOptionUp(optionList.get(row-1));
OptionDef optionDef = optionList.get(row-1);
addOption(optionDef.getText(),optionDef.getQuestionID(),row);
optionDef = optionList.get(row-2);
addOption(optionDef.getText(),optionDef.getQuestionID(),row-1);
break;
}
else if(sender == table.getWidget(row, 4)){
//Move down button.
if(row == (rowCount - 2))
return;
moveOptionDown(optionList.get(row-1));
OptionDef optionDef = optionList.get(row-1);
addOption(optionDef.getText(),optionDef.getQuestionID(),row);
optionDef = optionList.get(row);
addOption(optionDef.getText(),optionDef.getQuestionID(),row+1);
break;
}
}
}
}
/**
* Adds a new dynamic list option to the table.
*/
private TextBox addNewOption(){
table.removeRow(table.getRowCount() - 1);
TextBox textBox = addOption("","",table.getRowCount());
textBox.setFocus(true);
textBox.selectAll();
addAddButton();
addNewOptionDef();
return textBox;
}
/**
* Adds a new option to the table of dynamic options list.
*
* @param text the option text.
* @param binding the option binding.
* @param row the index of the row to add.
* @return the widget for editing text of the new option.
*/
private TextBox addOption(String text, String binding, int row){
TextBox txtText = new TextBox();
TextBox txtBinding = new TextBox();
txtText.setText(text);
txtBinding.setText(binding);
table.setWidget(row, 0,txtText);
table.setWidget(row, 1,txtBinding);
txtBinding.setEnabled(!Context.isStructureReadOnly());
PushButton button = new PushButton(FormUtil.createImage(Toolbar.images.delete()));
button.setTitle(LocaleText.get("deleteItem"));
button.addClickHandler(this);
table.setWidget(row, 2,button);
button = new PushButton(FormUtil.createImage(Toolbar.images.moveup()));
button.setTitle(LocaleText.get("moveUp"));
button.addClickHandler(this);
table.setWidget(row, 3,button);
button = new PushButton(FormUtil.createImage(Toolbar.images.movedown()));
button.setTitle(LocaleText.get("moveDown"));
button.addClickHandler(this);
table.setWidget(row, 4,button);
table.getFlexCellFormatter().setWidth(row, 0, "45%");
table.getFlexCellFormatter().setWidth(row, 1, "45%");
table.getFlexCellFormatter().setWidth(row, 2, "3.3%");
table.getFlexCellFormatter().setWidth(row, 3, "3.3%");
table.getFlexCellFormatter().setWidth(row, 4, "3.3%");
table.getWidget(row, 0).setWidth("100%");
table.getWidget(row, 1).setWidth("100%");
txtText.addChangeHandler(new ChangeHandler(){
public void onChange(ChangeEvent event){
updateText((TextBox)event.getSource());
}
});
txtText.addKeyDownHandler(new KeyDownHandler(){
public void onKeyDown(KeyDownEvent event) {
int keyCode = event.getNativeKeyCode();
if(keyCode == KeyCodes.KEY_ENTER || keyCode == KeyCodes.KEY_DOWN)
moveToNextWidget((Widget)event.getSource(),0,keyCode == KeyCodes.KEY_DOWN);
else if(keyCode == KeyCodes.KEY_UP)
moveToPrevWidget((Widget)event.getSource(),0);
}
});
txtText.addKeyUpHandler(new KeyUpHandler(){
public void onKeyUp(KeyUpEvent event) {
int keyCode = event.getNativeKeyCode();
if(!(keyCode == KeyCodes.KEY_ENTER || keyCode == KeyCodes.KEY_DOWN ||
keyCode == KeyCodes.KEY_DOWN || keyCode == KeyCodes.KEY_UP))
updateText((TextBox)event.getSource());
}
});
txtBinding.addChangeHandler(new ChangeHandler(){
public void onChange(ChangeEvent event){
updateBinding((TextBox)event.getSource());
}
});
txtBinding.addKeyDownHandler(new KeyDownHandler(){
public void onKeyDown(KeyDownEvent event) {
int keyCode = event.getNativeKeyCode();
if(keyCode == KeyCodes.KEY_ENTER || keyCode == KeyCodes.KEY_DOWN)
moveToNextWidget((Widget)event.getSource(),1,keyCode == KeyCodes.KEY_DOWN);
else if(keyCode == KeyCodes.KEY_UP)
moveToPrevWidget((Widget)event.getSource(),1);
}
});
return txtText;
}
/**
* Updates the selected object with the new text as typed by the user.
*/
private void updateText(TextBox txtText){
int rowCount = table.getRowCount();
for(int row = 1; row < rowCount; row++){
if(txtText == table.getWidget(row, 0)){
OptionDef optionDef = null;
if(optionList.size() > row-1)
optionDef = optionList.get(row-1);
if(optionDef == null)
optionDef = addNewOptionDef();
String orgTextDefBinding = FormDesignerUtil.getXmlTagName(optionDef.getText());
optionDef.setText(txtText.getText());
if(!Context.isStructureReadOnly()){
//automatically set the binding, if empty.
TextBox txtBinding = (TextBox)table.getWidget(row, 1);
String binding = txtBinding.getText();
//if(binding == null || binding.trim().length() == 0){
if(binding == null || binding.trim().length() == 0 || binding.equals(orgTextDefBinding)){
txtBinding.setText(FormDesignerUtil.getXmlTagName(optionDef.getText()));
optionDef.setQuestionID(txtBinding.getText());
optionDef.setItextId(optionDef.getQuestionID());
}
}
break;
}
}
}
/**
* Adds a new option definition object.
*
* @return the new option definition object.
*/
private OptionDef addNewOptionDef(){
OptionDef optionDef = new OptionDef(parentQuestionDef);
optionDef.setId(dynamicOptionDef.getNextOptionId());
dynamicOptionDef.setNextOptionId(optionDef.getId() + 1);
optionList.add(optionDef);
return optionDef;
}
/**
* Updates the selected object with the new binding as typed by the user.
*/
private void updateBinding(TextBox txtBinding){
int rowCount = table.getRowCount();
for(int row = 1; row < rowCount; row++){
if(txtBinding == table.getWidget(row, 1)){
OptionDef optionDef = null;
if(optionList.size() > row-1)
optionDef = optionList.get(row-1);
if(optionDef == null)
optionDef = addNewOptionDef();
optionDef.setQuestionID(txtBinding.getText());
optionDef.setItextId(optionDef.getQuestionID());
break;
}
}
}
/**
* Adds the add new button to the table widget.
*/
private void addAddButton(){
FlexCellFormatter cellFormatter = table.getFlexCellFormatter();
int row = table.getRowCount();
cellFormatter.setColSpan(row, 0, 5);
cellFormatter.setHorizontalAlignment(row, 0, HasHorizontalAlignment.ALIGN_CENTER);
table.setWidget(row, 0, btnAdd);
}
/**
* Moves input focus to the next widget.
*
* @param sender the widget after which to move the input focus.
* @param col the index of the column which currently has input focus.
* @param sameCol set to true to move to the next widget in the same column.
*/
private void moveToNextWidget(Widget sender, int col, boolean sameCol){
if(sameCol){
int rowCount = table.getRowCount();
for(int row = 1; row < rowCount; row++){
if(sender == table.getWidget(row, col)){
if(row == (rowCount - 2))
return;
TextBox textBox = ((TextBox)table.getWidget(row + 1, col));
textBox.setFocus(true);
textBox.selectAll();
break;
}
}
}
else{
int rowCount = table.getRowCount();
for(int row = 1; row < rowCount; row++){
if(sender == table.getWidget(row, col)){
TextBox textBox = ((TextBox)table.getWidget(row, col));
if(col == 1){
if(row == (rowCount - 2)){
if(textBox.getText() != null && textBox.getText().trim().length() > 0)
addNewOption();
return;
}
row++;
col = 1; //0;
}
else{
if(textBox.getText() == null || textBox.getText().trim().length() == 0)
return;
else if(row == (rowCount - 2)){
addNewOption();
return;
}
else
row++;
col = 0; //1;
}
textBox = ((TextBox)table.getWidget(row, col));
textBox.setFocus(true);
textBox.selectAll();
break;
}
}
}
}
/**
* Moves input focus to the widget before.
*
* @param sender the widget before which to move the input focus.
* @param col the index of the column which currently has input focus.
*/
private void moveToPrevWidget(Widget sender, int col){
int rowCount = table.getRowCount();
//Starting from index 1 since 0 is the header row.
for(int row = 1; row < rowCount; row++){
if(sender == table.getWidget(row, col)){
if(row == 1)
return;
TextBox textBox = ((TextBox)table.getWidget(row - 1, col));
textBox.setFocus(true);
textBox.selectAll();
break;
}
}
}
/**
* Moves an option one position upwards.
*
* @param optionDef the option to move.
*/
public void moveOptionUp(OptionDef optionDef){
List optns = optionList;
int index = optns.indexOf(optionDef);
optns.remove(optionDef);
Node parentNode = null;
if(optionDef.getControlNode() != null){
parentNode = optionDef.getControlNode().getParentNode();
parentNode.removeChild(optionDef.getControlNode());
}
OptionDef currentOptionDef;
List<OptionDef> list = new ArrayList<OptionDef>();
//Remove all from index before selected all the way downwards
while(optns.size() >= index){
currentOptionDef = (OptionDef)optns.get(index-1);
list.add(currentOptionDef);
optns.remove(currentOptionDef);
}
optns.add(optionDef);
for(int i=0; i<list.size(); i++){
if(i == 0){
OptionDef optnDef = (OptionDef)list.get(i);
if(parentNode != null && optnDef.getControlNode() != null && optionDef.getControlNode() != null)
parentNode.insertBefore(optionDef.getControlNode(), optnDef.getControlNode());
}
optns.add(list.get(i));
}
}
/**
* Moves an option one position downwards.
*
* @param optionDef the option to move.
*/
public void moveOptionDown(OptionDef optionDef){
List optns = optionList;
int index = optns.indexOf(optionDef);
optns.remove(optionDef);
Node parentNode = null;
if(optionDef.getControlNode() != null){
parentNode = optionDef.getControlNode().getParentNode();
parentNode.removeChild(optionDef.getControlNode());
}
OptionDef currentItem; // = parent.getChild(index - 1);
List<OptionDef> list = new ArrayList<OptionDef>();
//Remove all otions below selected index
while(optns.size() > 0 && optns.size() > index){
currentItem = (OptionDef)optns.get(index);
list.add(currentItem);
optns.remove(currentItem);
}
for(int i=0; i<list.size(); i++){
if(i == 1){
optns.add(optionDef); //Add after the first item but before the current (second).
OptionDef optnDef = getNextSavedOption(list,i); //(OptionDef)list.get(i);
if(optnDef.getControlNode() != null && optionDef.getControlNode() != null)
parentNode.insertBefore(optionDef.getControlNode(), optnDef.getControlNode());
else if(parentNode != null)
parentNode.appendChild(optionDef.getControlNode());
}
optns.add(list.get(i));
}
//If was second last and hence becoming last
if(list.size() == 1){
optns.add(optionDef);
if(optionDef.getControlNode() != null)
parentNode.appendChild(optionDef.getControlNode());
}
}
/**
* Gets the next option which has been converted to xforms and
* hence attached to an xforms document node, starting at a given
* index in a list of options.
*
* @param options the list of options.
* @param index the index to start from in the option list.
* @return the option.
*/
private OptionDef getNextSavedOption(List options, int index){
for(int i=index; i<options.size(); i++){
OptionDef optionDef = (OptionDef)options.get(i);
if(optionDef.getControlNode() != null)
return optionDef;
}
return (OptionDef)options.get(index);
}
}