/*
* Copyright (c) 2011, Michael Grossmann, Nikolaus Moll
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the jo-widgets.org nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
package org.jowidgets.impl.widgets.composed;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.jowidgets.api.layout.tablelayout.ITableLayout;
import org.jowidgets.api.layout.tablelayout.ITableLayoutBuilder;
import org.jowidgets.api.layout.tablelayout.ITableLayoutBuilder.ColumnMode;
import org.jowidgets.api.toolkit.Toolkit;
import org.jowidgets.api.widgets.IButton;
import org.jowidgets.api.widgets.IComposite;
import org.jowidgets.api.widgets.IControl;
import org.jowidgets.api.widgets.IInputComponentValidationLabel;
import org.jowidgets.api.widgets.IInputControl;
import org.jowidgets.api.widgets.IInputField;
import org.jowidgets.api.widgets.IScrollComposite;
import org.jowidgets.api.widgets.ITextLabel;
import org.jowidgets.api.widgets.blueprint.IButtonBluePrint;
import org.jowidgets.api.widgets.blueprint.IInputComponentValidationLabelBluePrint;
import org.jowidgets.api.widgets.blueprint.factory.IBluePrintFactory;
import org.jowidgets.api.widgets.descriptor.ICollectionInputControlDescriptor;
import org.jowidgets.common.color.IColorConstant;
import org.jowidgets.common.types.Dimension;
import org.jowidgets.common.types.Modifier;
import org.jowidgets.common.types.Position;
import org.jowidgets.common.types.Rectangle;
import org.jowidgets.common.types.VirtualKey;
import org.jowidgets.common.widgets.controller.IActionListener;
import org.jowidgets.common.widgets.controller.IInputListener;
import org.jowidgets.common.widgets.controller.IKeyEvent;
import org.jowidgets.common.widgets.factory.ICustomWidgetCreator;
import org.jowidgets.i18n.api.IMessage;
import org.jowidgets.i18n.api.MessageReplacer;
import org.jowidgets.impl.layout.ListLayout;
import org.jowidgets.impl.layout.tablelayout.TableRowLayout;
import org.jowidgets.tools.controller.InputObservable;
import org.jowidgets.tools.controller.KeyAdapter;
import org.jowidgets.tools.layout.MigLayoutFactory;
import org.jowidgets.tools.validation.ValidationCache;
import org.jowidgets.tools.validation.ValidationCache.IValidationResultCreator;
import org.jowidgets.tools.widgets.blueprint.BPF;
import org.jowidgets.tools.widgets.wrapper.CompositeWrapper;
import org.jowidgets.tools.widgets.wrapper.ControlWrapper;
import org.jowidgets.validation.IValidationConditionListener;
import org.jowidgets.validation.IValidationResult;
import org.jowidgets.validation.IValidationResultBuilder;
import org.jowidgets.validation.IValidator;
import org.jowidgets.validation.ValidationResult;
import org.jowidgets.validation.tools.CompoundValidator;
public class CollectionInputControlImpl<INPUT_TYPE> extends ControlWrapper implements IInputControl<Collection<INPUT_TYPE>> {
private static final IMessage ELEMENT = Messages.getMessage("CollectionInputControlImpl.element");
private static final IMessage REMOVE_ELEMENT = Messages.getMessage("CollectionInputControlImpl.remove_element");
private static final IMessage PLEASE_EDIT_ELEMENT = Messages.getMessage("CollectionInputControlImpl.please_edit_element");
private final IBluePrintFactory bpf;
private final ITableLayout tableCommon;
private final IScrollComposite scrollComposite;
private final IComposite composite;
private final IInputComponentValidationLabelBluePrint validationLabelBp;
private final IButtonBluePrint removeButtonBp;
private final Dimension removeButtonSize;
private final ICustomWidgetCreator<IInputControl<INPUT_TYPE>> widgetCreator;
private final Dimension validationLabelSize;
private final ValuesContainer valuesContainer;
private final IComposite addValueComposite;
private final IButton addButton;
private final InputObservable inputObservable;
private final ValidationCache validationCache;
private final CompoundValidator<Collection<INPUT_TYPE>> compoundValidator;
private int lastRowCount;
private boolean programmaticUpdate;
private boolean editable;
public CollectionInputControlImpl(final IComposite rootComposite, final ICollectionInputControlDescriptor<INPUT_TYPE> setup) {
super(rootComposite);
rootComposite.setLayout(MigLayoutFactory.growingInnerCellLayout());
this.scrollComposite = rootComposite.add(BPF.scrollComposite(), MigLayoutFactory.GROWING_CELL_CONSTRAINTS);
scrollComposite.setLayout(MigLayoutFactory.growingCellLayout());
this.composite = scrollComposite.add(BPF.composite(), MigLayoutFactory.GROWING_CELL_CONSTRAINTS);
this.bpf = Toolkit.getBluePrintFactory();
this.inputObservable = new InputObservable();
this.compoundValidator = new CompoundValidator<Collection<INPUT_TYPE>>();
//Get some settings from setup
this.removeButtonBp = bpf.button().setSetup(setup.getRemoveButton());
this.removeButtonSize = setup.getRemoveButtonSize();
this.widgetCreator = setup.getElementWidgetCreator();
final IButtonBluePrint addButtonBp = bpf.button().setSetup(setup.getAddButton());
if (setup.getValidationLabel() != null) {
this.validationLabelBp = bpf.inputComponentValidationLabel();
validationLabelBp.setSetup(setup.getValidationLabel());
}
else {
this.validationLabelBp = null;
}
this.validationLabelSize = setup.getValidationLabelSize();
this.validationCache = new ValidationCache(new IValidationResultCreator() {
@Override
public IValidationResult createValidationResult() {
final IValidationResultBuilder builder = ValidationResult.builder();
int index = 1;
for (final Row row : valuesContainer.rows) {
final IInputControl<INPUT_TYPE> inputControl = row.inputControl;
final IValidationResult controlResult = inputControl.validate()
.withContext(MessageReplacer.replace(ELEMENT.get(), String.valueOf(index)));
if (inputControl.hasModifications() && !controlResult.isValid()) {
builder.addResult(controlResult);
}
else if (!controlResult.isValid()) {
builder.addResult(
ValidationResult
.infoError(MessageReplacer.replace(PLEASE_EDIT_ELEMENT.get(), String.valueOf(index))));
}
index++;
}
builder.addResult(compoundValidator.validate(getValue()));
return builder.build();
}
});
final int columns = getColumnCount(setup);
final int maxButtonWidth = Math.max(setup.getRemoveButtonSize().getWidth(), setup.getAddButtonSize().getWidth());
final ITableLayoutBuilder rowLayoutCommonBuilder = Toolkit.getLayoutFactoryProvider().tableLayoutBuilder();
rowLayoutCommonBuilder.layoutMinRows(1);
rowLayoutCommonBuilder.columnCount(columns);
rowLayoutCommonBuilder.gap(3);
rowLayoutCommonBuilder.gapAfterColumn(columns - 1, 8);
rowLayoutCommonBuilder.columnMode(2, ColumnMode.GROWING);
rowLayoutCommonBuilder.fixedColumnWidth(1, maxButtonWidth);
if (columns > 3) {
rowLayoutCommonBuilder.fixedColumnWidth(3, 20);
}
this.tableCommon = rowLayoutCommonBuilder.build();
composite.setLayout(Toolkit.getLayoutFactoryProvider().listLayout());
valuesContainer = new ValuesContainer(composite.add(bpf.composite()));
this.addValueComposite = composite.add(bpf.composite());
addValueComposite.setLayout(tableCommon.rowBuilder().build());
this.addButton = addValueComposite.add(addButtonBp, "index 1"); //$NON-NLS-1$
addButton.setPreferredSize(setup.getAddButtonSize());
addButton.addActionListener(new IActionListener() {
@Override
public void actionPerformed() {
valuesContainer.addRow().inputControl.requestFocus();
}
});
if (setup.getValidator() != null) {
compoundValidator.addValidator(setup.getValidator());
}
setValue(setup.getValue());
this.editable = setup.isEditable();
if (!setup.isEditable()) {
setEditable(false);
}
tableCommon.validate();
resetModificationState();
}
@Override
protected IComposite getWidget() {
return (IComposite) super.getWidget();
}
private int getColumnCount(final ICollectionInputControlDescriptor<INPUT_TYPE> setup) {
if (setup.getValidationLabel() == null) {
return 3;
}
else {
return 4;
}
}
private void updateLayout() {
if (addValueComposite != null) {
getWidget().layoutBegin();
getWidget().layoutEnd();
if (getParent() != null) {
getParent().layoutBegin();
getParent().layoutEnd();
}
tableCommon.validate();
addValueComposite.layoutBegin();
addValueComposite.layoutEnd();
valuesContainer.updateRowsLayout();
}
}
@Override
public void setEditable(final boolean editable) {
this.editable = editable;
valuesContainer.setEditable(editable);
addButton.setEnabled(editable);
}
@Override
public boolean isEditable() {
return editable;
}
@Override
public void addValidator(final IValidator<Collection<INPUT_TYPE>> validator) {
compoundValidator.addValidator(validator);
}
@Override
public boolean hasModifications() {
boolean result = lastRowCount != valuesContainer.rows.size();
result = result || isControlModified();
return result;
}
private boolean isControlModified() {
for (final Row row : valuesContainer.rows) {
if (row.inputControl.hasModifications()) {
return true;
}
}
return false;
}
@Override
public void resetModificationState() {
lastRowCount = valuesContainer.rows.size();
for (final Row row : valuesContainer.rows) {
row.inputControl.resetModificationState();
}
}
@Override
public IValidationResult validate() {
return validationCache.validate();
}
@Override
public void addValidationConditionListener(final IValidationConditionListener listener) {
validationCache.addValidationConditionListener(listener);
}
@Override
public void removeValidationConditionListener(final IValidationConditionListener listener) {
validationCache.removeValidationConditionListener(listener);
}
@Override
public void addInputListener(final IInputListener listener) {
inputObservable.addInputListener(listener);
}
@Override
public void removeInputListener(final IInputListener listener) {
inputObservable.removeInputListener(listener);
}
private void fireInputChanged() {
if (!programmaticUpdate) {
inputObservable.fireInputChanged();
//TODO MG,NM Review the validation cache must be set dirty, if rows was added
//or removed. Is there may be a better place do do this?
validationCache.setDirty();
}
}
@Override
public void setValue(final Collection<INPUT_TYPE> value) {
programmaticUpdate = true;
valuesContainer.setValue(value);
programmaticUpdate = false;
validationCache.setDirty();
updateLayout();
}
@Override
public Collection<INPUT_TYPE> getValue() {
return valuesContainer.getValue();
}
private final class Row extends CompositeWrapper {
private final IInputControl<INPUT_TYPE> inputControl;
private final IButton removeButton;
private final IInputComponentValidationLabel validationLabel;
private final TableRowLayout layout;
private final ITextLabel valueIndex;
private Row(final IComposite container) {
super(container);
layout = new TableRowLayout(container, tableCommon, false);
setLayout(layout);
final int index = valuesContainer.getValueCount() + 1;
valueIndex = add(bpf.textLabel("").alignRight()); //$NON-NLS-1$
removeButton = add(removeButtonBp);
removeButton.setPreferredSize(removeButtonSize);
inputControl = add(widgetCreator);
inputControl.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(final IKeyEvent event) {
final int index = valuesContainer.indexOf(Row.this);
if (VirtualKey.ENTER.equals(event.getVirtualKey())) {
final int newIndex;
if (event.getModifier().contains(Modifier.SHIFT)) {
newIndex = index;
}
else {
newIndex = index + 1;
}
final Row row = valuesContainer.addRow(newIndex);
row.inputControl.requestFocus();
}
}
});
if (inputControl instanceof IInputField) {
final IInputField<INPUT_TYPE> inputField = (IInputField<INPUT_TYPE>) inputControl;
inputControl.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(final IKeyEvent event) {
final int index = valuesContainer.indexOf(Row.this);
if (VirtualKey.BACK_SPACE.equals(event.getVirtualKey())
|| VirtualKey.DELETE.equals(event.getVirtualKey())) {
final String text = inputField.getText();
boolean removeControl = (text == null || "".equals(text)); //$NON-NLS-1$
if (!removeControl) {
final Object value = inputField.getValue();
if (value instanceof String) {
removeControl = ("".equals(value)); //$NON-NLS-1$
}
}
if (removeControl) {
int delta = 0;
if (VirtualKey.BACK_SPACE.equals(event.getVirtualKey())) {
delta = -1;
}
valuesContainer.removeRow(index);
final Row row = valuesContainer
.getRow(Math.min(Math.max(0, index + delta), valuesContainer.getValueCount() - 1));
if (row != null) {
row.inputControl.requestFocus();
row.scrollRectToRow();
}
else {
addButton.requestFocus();
}
}
}
else if (VirtualKey.ARROW_UP.equals(event.getVirtualKey())) {
if (index > 0) {
final Row row = valuesContainer.getRow(index - 1);
row.inputControl.requestFocus();
row.scrollRectToRow();
}
}
else if (VirtualKey.ARROW_DOWN.equals(event.getVirtualKey())) {
// size - 1 because of add button
if (index < valuesContainer.rows.size() - 1) {
final Row row = valuesContainer.getRow(index + 1);
row.inputControl.requestFocus();
row.scrollRectToRow();
}
}
}
});
}
inputControl.addInputListener(new IInputListener() {
@Override
public void inputChanged() {
fireInputChanged();
}
});
inputControl.addValidationConditionListener(new IValidationConditionListener() {
@Override
public void validationConditionsChanged() {
validationCache.setDirty();
}
});
removeButton.addActionListener(new IActionListener() {
@Override
public void actionPerformed() {
final int index = valuesContainer.indexOf(Row.this);
valuesContainer.removeRow(index);
}
});
if (validationLabelBp != null) {
validationLabel = add(validationLabelBp.setInputComponent(inputControl));
validationLabel.setPreferredSize(validationLabelSize);
}
else {
validationLabel = null;
}
setTabOrder(Collections.singletonList(inputControl));
setValueIndex(index);
}
public void setValueIndex(final int index) {
valueIndex.setText(String.valueOf(index));
removeButton.setToolTipText(MessageReplacer.replace(REMOVE_ELEMENT.get(), String.valueOf(index)));
layout.invalidateControl(valueIndex);
}
public void setEditable(final boolean editable) {
inputControl.setEditable(editable);
removeButton.setEnabled(editable);
}
public void removeLayout() {
layout.remove();
}
private INPUT_TYPE getValue() {
return inputControl.getValue();
}
private void setValue(final INPUT_TYPE value) {
inputControl.setValue(value);
}
private IControl getControl() {
return getWidget();
}
private void scrollRectToRow() {
final Position shiftedPos = scrollComposite.fromComponent(composite, getPosition());
scrollComposite.scrollRectToVisible(new Rectangle(shiftedPos, getSize()));
}
}
private class ValuesContainer extends CompositeWrapper {
private final List<Row> rows;
ValuesContainer(final IComposite widget) {
super(widget);
this.rows = new LinkedList<Row>();
setLayout(new ListLayout(this, new IColorConstant[0]));
}
public Collection<INPUT_TYPE> getValue() {
final List<INPUT_TYPE> result = new LinkedList<INPUT_TYPE>();
for (final Row row : rows) {
result.add(row.getValue());
}
return result;
}
public int getValueCount() {
return rows.size();
}
public int indexOf(final Row row) {
return rows.indexOf(row);
}
public void setValue(final Collection<INPUT_TYPE> value) {
clear();
if (value != null) {
for (final INPUT_TYPE currentValue : value) {
addRow().setValue(currentValue);
}
}
}
public void updateRowsLayout() {
for (final Row row : rows) {
row.layout.layout();
}
}
public void setEditable(final boolean editable) {
for (final Row row : rows) {
row.setEditable(editable);
}
}
public Row addRow() {
final Row row = new Row(add(bpf.composite()));
rows.add(row);
updateLayout();
scrollComposite.scrollToBottom();
fireInputChanged();
return row;
}
public Row addRow(final int index) {
layoutBegin();
final Row result = new Row(add(index, bpf.composite(), null));
rows.add(index, result);
for (int i = index; i < rows.size(); i++) {
final Row row = rows.get(i);
row.setValueIndex(i + 1);
}
layoutEnd();
updateLayout();
if (index == rows.size() - 1) {
scrollComposite.scrollToBottom();
}
else {
result.scrollRectToRow();
}
fireInputChanged();
return result;
}
public void removeRow(final int index) {
layoutBegin();
final Row removedRow = rows.get(index);
rows.remove(index);
for (int i = index; i < rows.size(); i++) {
final Row row = rows.get(i);
row.setValueIndex(i + 1);
}
removedRow.removeLayout();
remove(removedRow.getControl());
layoutEnd();
updateLayout();
fireInputChanged();
final Row row = valuesContainer.getRow(Math.min(Math.max(0, index), valuesContainer.getValueCount() - 1));
if (row != null) {
row.inputControl.requestFocus();
}
else {
addButton.requestFocus();
}
}
public Row getRow(final int index) {
if (index < 0 || index >= rows.size()) {
return null;
}
return rows.get(index);
}
public void clear() {
layoutBegin();
for (final Row row : rows) {
row.removeLayout();
remove(row.getControl());
}
layoutEnd();
rows.clear();
updateLayout();
fireInputChanged();
}
}
}