package name.abuchen.portfolio.ui.util;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.core.databinding.AggregateValidationStatus;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.beans.BeanProperties;
import org.eclipse.core.databinding.beans.PojoProperties;
import org.eclipse.core.databinding.conversion.IConverter;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.validation.IValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.databinding.swt.ISWTObservableValue;
import org.eclipse.jface.databinding.swt.WidgetProperties;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.Text;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.money.CurrencyUnit;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.ui.Messages;
import name.abuchen.portfolio.util.Isin;
public class BindingHelper
{
private static final class StringToCurrencyUnitConverter implements IConverter
{
@Override
public Object getToType()
{
return CurrencyUnit.class;
}
@Override
public Object getFromType()
{
return String.class;
}
@Override
public Object convert(Object fromObject)
{
return fromObject == null ? CurrencyUnit.EMPTY : CurrencyUnit.getInstance((String) fromObject);
}
}
private static final class CurrencyUnitToStringConverter implements IConverter
{
@Override
public Object getToType()
{
return String.class;
}
@Override
public Object getFromType()
{
return CurrencyUnit.class;
}
@Override
public Object convert(Object fromObject)
{
return CurrencyUnit.EMPTY.equals(fromObject) ? null : ((CurrencyUnit) fromObject).getCurrencyCode();
}
}
public abstract static class Model
{
private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
private Client client;
public Model()
{}
public Model(Client client)
{
this.client = client;
}
public void addPropertyChangeListener(PropertyChangeListener listener)
{
propertyChangeSupport.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener)
{
propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener)
{
propertyChangeSupport.removePropertyChangeListener(listener);
}
public Client getClient()
{
return client;
}
protected void firePropertyChange(String attribute, Object oldValue, Object newValue)
{
propertyChangeSupport.firePropertyChange(attribute, oldValue, newValue);
}
protected void firePropertyChange(String attribute, long oldValue, long newValue)
{
propertyChangeSupport.firePropertyChange(attribute, oldValue, newValue);
}
public abstract void applyChanges();
}
private static final class StatusTextConverter implements IConverter
{
@Override
public Object getToType()
{
return String.class;
}
@Override
public Object getFromType()
{
return IStatus.class;
}
@Override
public Object convert(Object fromObject)
{
IStatus status = (IStatus) fromObject;
return status.isOK() ? "" : status.getMessage(); //$NON-NLS-1$
}
}
class ModelStatusListener
{
public void setStatus(IStatus status)
{
onValidationStatusChanged(status);
}
public IStatus getStatus()
{
// irrelevant
return ValidationStatus.ok();
}
}
private Model model;
private ModelStatusListener listener = new ModelStatusListener();
private DataBindingContext context;
/** average char width needed to resize input fields on length */
private int averageCharWidth = -1;
public BindingHelper(Model model)
{
this.model = model;
this.context = new DataBindingContext();
context.bindValue(PojoProperties.value("status").observe(listener), //$NON-NLS-1$
new AggregateValidationStatus(context, AggregateValidationStatus.MAX_SEVERITY));
}
public void onValidationStatusChanged(IStatus status)
{}
public DataBindingContext getBindingContext()
{
return context;
}
public final void createErrorLabel(Composite editArea)
{
// error label
Label errorLabel = new Label(editArea, SWT.NONE);
GridDataFactory.fillDefaults().span(2, 1).grab(true, false).applyTo(errorLabel);
// error label
context.bindValue(WidgetProperties.text().observe(errorLabel), //
new AggregateValidationStatus(context, AggregateValidationStatus.MAX_SEVERITY), //
null, //
new UpdateValueStrategy().setConverter(new StatusTextConverter()));
}
public final void createLabel(Composite editArea, String text)
{
Label lblTransactionType = new Label(editArea, SWT.NONE);
lblTransactionType.setText(text);
GridDataFactory.fillDefaults().span(2, 1).grab(true, false).applyTo(lblTransactionType);
}
public final void bindLabel(Composite editArea, String property)
{
Label label = new Label(editArea, SWT.NONE);
context.bindValue(WidgetProperties.text().observe(label), BeanProperties.value(property).observe(model));
GridDataFactory.fillDefaults().span(1, 1).grab(true, false).applyTo(label);
}
public final void bindSpinner(Composite editArea, String label, String property, int min, int max, int selection,
int increment)
{
Label l = new Label(editArea, SWT.NONE);
l.setText(label);
Spinner spinner = new Spinner(editArea, SWT.BORDER);
spinner.setMinimum(min);
spinner.setMaximum(max);
spinner.setSelection(selection);
spinner.setIncrement(increment);
GridDataFactory.fillDefaults().align(SWT.BEGINNING, SWT.FILL)
.hint(5 * getAverageCharWidth(spinner), SWT.DEFAULT).applyTo(spinner);
context.bindValue(WidgetProperties.selection().observe(spinner), BeanProperties.value(property).observe(model));
}
public final ComboViewer bindComboViewer(Composite editArea, String label, String property,
IBaseLabelProvider labelProvider, Object input)
{
return bindComboViewer(editArea, label, property, labelProvider, null, input);
}
public final ComboViewer bindComboViewer(Composite editArea, String label, String property,
IBaseLabelProvider labelProvider, IValidator validator, Object input)
{
Label l = new Label(editArea, SWT.NONE);
l.setText(label);
ComboViewer combo = new ComboViewer(editArea, SWT.READ_ONLY);
combo.setContentProvider(ArrayContentProvider.getInstance());
combo.setLabelProvider(labelProvider);
combo.setInput(input);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(combo.getControl());
UpdateValueStrategy strategy = new UpdateValueStrategy();
if (validator != null)
strategy.setAfterConvertValidator(validator);
context.bindValue(ViewersObservables.observeSingleSelection(combo), //
BeanProperties.value(property).observe(model), strategy, null);
return combo;
}
public final ComboViewer bindCurrencyCodeCombo(Composite editArea, String label, String property)
{
Label l = new Label(editArea, SWT.NONE);
l.setText(label);
ComboViewer combo = new ComboViewer(editArea, SWT.READ_ONLY);
combo.setContentProvider(ArrayContentProvider.getInstance());
combo.setLabelProvider(new LabelProvider());
List<CurrencyUnit> currencies = new ArrayList<>();
currencies.add(CurrencyUnit.EMPTY);
currencies.addAll(CurrencyUnit.getAvailableCurrencyUnits().stream().sorted().collect(Collectors.toList()));
combo.setInput(currencies);
GridDataFactory.fillDefaults().align(SWT.BEGINNING, SWT.FILL).applyTo(combo.getControl());
UpdateValueStrategy targetToModel = new UpdateValueStrategy();
targetToModel.setConverter(new CurrencyUnitToStringConverter());
UpdateValueStrategy modelToTarget = new UpdateValueStrategy();
modelToTarget.setConverter(new StringToCurrencyUnitConverter());
context.bindValue(ViewersObservables.observeSingleSelection(combo), //
BeanProperties.value(property).observe(model), targetToModel, modelToTarget);
return combo;
}
public final void bindDatePicker(Composite editArea, String label, String property)
{
Label l = new Label(editArea, SWT.NONE);
l.setText(label);
DateTimePicker boxDate = new DateTimePicker(editArea);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(boxDate.getControl());
context.bindValue(new SimpleDateTimeSelectionProperty().observe(boxDate.getControl()),
BeanProperties.value(property).observe(model), new UpdateValueStrategy() //
.setAfterConvertValidator(value -> {
return value != null ? ValidationStatus.ok()
: ValidationStatus.error(MessageFormat.format(
Messages.MsgDialogInputRequired, label));
}),
null);
}
public final Control bindMandatoryQuoteInput(Composite editArea, final String label, String property)
{
Text txtValue = createTextInput(editArea, label);
bindMandatoryDecimalInput(label, property, txtValue, Values.Quote);
return txtValue;
}
private void bindMandatoryDecimalInput(final String label, String property, Text txtValue, Values<?> type)
{
StringToCurrencyConverter converter = new StringToCurrencyConverter(type);
UpdateValueStrategy input2model = new UpdateValueStrategy() //
.setAfterGetValidator(converter) //
.setConverter(converter) //
.setAfterConvertValidator(value -> {
Long v = (Long) value;
return v != null && v.longValue() > 0 ? ValidationStatus.ok()
: ValidationStatus.error(MessageFormat
.format(Messages.MsgDialogInputRequired, label));
});
context.bindValue(WidgetProperties.text(SWT.Modify).observe(txtValue), //
BeanProperties.value(property).observe(model), //
input2model, // ,
new UpdateValueStrategy().setConverter(new CurrencyToStringConverter(type)));
}
private Text createTextInput(Composite editArea, final String label)
{
return createTextInput(editArea, label, SWT.NONE, SWT.DEFAULT);
}
private Text createTextInput(Composite editArea, final String label, int style, int lenghtInCharacters)
{
Label l = new Label(editArea, SWT.NONE);
l.setText(label);
final Text txtValue = new Text(editArea, SWT.BORDER | style);
txtValue.addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
txtValue.selectAll();
}
});
if (lenghtInCharacters == SWT.DEFAULT)
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(txtValue);
else
GridDataFactory.fillDefaults().align(SWT.BEGINNING, SWT.FILL)
.hint((lenghtInCharacters + 5) * getAverageCharWidth(txtValue), SWT.DEFAULT)
.applyTo(txtValue);
return txtValue;
}
public final Control bindMandatoryLongInput(Composite editArea, final String label, String property)
{
Text txtValue = createTextInput(editArea, label);
context.bindValue(WidgetProperties.text(SWT.Modify).observe(txtValue), //
BeanProperties.value(property).observe(model), //
new UpdateValueStrategy().setAfterConvertValidator(new IValidator()
{
@Override
public IStatus validate(Object value)
{
Long v = (Long) value;
return v != null && v.longValue() > 0 ? ValidationStatus.ok()
: ValidationStatus.error(MessageFormat
.format(Messages.MsgDialogInputRequired, label));
}
}), //
null);
return txtValue;
}
public final IObservableValue bindStringInput(Composite editArea, final String label, String property)
{
return bindStringInput(editArea, label, property, SWT.NONE, SWT.DEFAULT);
}
public final IObservableValue bindStringInput(Composite editArea, final String label, String property, int style)
{
return bindStringInput(editArea, label, property, style, SWT.DEFAULT);
}
public final IObservableValue bindStringInput(Composite editArea, final String label, String property, int style,
int lenghtInCharacters)
{
Text txtValue = createTextInput(editArea, label, style, lenghtInCharacters);
ISWTObservableValue observeText = WidgetProperties.text(SWT.Modify).observe(txtValue);
context.bindValue(observeText, BeanProperties.value(property).observe(model));
return observeText;
}
public final Control bindMandatoryStringInput(Composite editArea, final String label, String property)
{
Text txtValue = createTextInput(editArea, label);
context.bindValue(WidgetProperties.text(SWT.Modify).observe(txtValue), //
BeanProperties.value(property).observe(model), //
new UpdateValueStrategy().setAfterConvertValidator(new IValidator()
{
@Override
public IStatus validate(Object value)
{
String v = (String) value;
return v != null && v.trim().length() > 0 ? ValidationStatus.ok()
: ValidationStatus.error(MessageFormat
.format(Messages.MsgDialogInputRequired, label));
}
}), //
null);
return txtValue;
}
public final Control bindISINInput(Composite editArea, final String label, String property)
{
Text txtValue = createTextInput(editArea, label, SWT.NONE, 12);
txtValue.setTextLimit(12);
context.bindValue(WidgetProperties.text(SWT.Modify).observe(txtValue), //
BeanProperties.value(property).observe(model), //
new UpdateValueStrategy().setAfterConvertValidator(new IValidator()
{
@Override
public IStatus validate(Object value)
{
String v = (String) value;
return v == null || v.trim().length() == 0 || Isin.isValid(v) ? ValidationStatus.ok()
: ValidationStatus.error(MessageFormat
.format(Messages.MsgDialogNotAValidISIN, label));
}
}), //
null);
return txtValue;
}
public final Control bindBooleanInput(Composite editArea, final String label, String property)
{
Label l = new Label(editArea, SWT.NONE);
l.setText(label);
final Button btnCheckbox = new Button(editArea, SWT.CHECK);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(btnCheckbox);
context.bindValue(WidgetProperties.selection().observe(btnCheckbox), //
BeanProperties.value(property).observe(model));
return btnCheckbox;
}
private int getAverageCharWidth(Control control)
{
if (averageCharWidth > 0)
return averageCharWidth;
GC gc = new GC(control);
FontMetrics fm = gc.getFontMetrics();
this.averageCharWidth = fm.getAverageCharWidth();
gc.dispose();
return averageCharWidth;
}
}