package name.abuchen.portfolio.ui.wizards.security; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.core.databinding.Binding; import org.eclipse.core.databinding.UpdateValueStrategy; import org.eclipse.core.databinding.ValidationStatusProvider; import org.eclipse.core.databinding.beans.BeanProperties; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.validation.IValidator; import org.eclipse.core.databinding.validation.MultiValidator; import org.eclipse.core.databinding.validation.ValidationStatus; import org.eclipse.core.runtime.IStatus; import org.eclipse.jface.databinding.swt.WidgetProperties; import org.eclipse.jface.databinding.viewers.ViewersObservables; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.resource.FontDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ComboViewer; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Spinner; import name.abuchen.portfolio.model.Classification; import name.abuchen.portfolio.model.Taxonomy; import name.abuchen.portfolio.money.Values; import name.abuchen.portfolio.ui.Images; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.util.BindingHelper; import name.abuchen.portfolio.ui.wizards.security.EditSecurityModel.ClassificationLink; import name.abuchen.portfolio.ui.wizards.security.EditSecurityModel.TaxonomyDesignation; public class SecurityTaxonomyPage extends AbstractPage { private static final class ClassificationNotTwiceValidator extends MultiValidator { private final List<IObservableValue> observables; private ClassificationNotTwiceValidator(List<IObservableValue> observables) { this.observables = observables; } @Override protected IStatus validate() { if (observables.isEmpty()) return ValidationStatus.ok(); Set<Classification> selected = new HashSet<Classification>(); for (IObservableValue value : observables) { Classification classification = (Classification) value.getValue(); if (!selected.add(classification)) return ValidationStatus.error(MessageFormat.format( Messages.EditWizardMasterDataMsgDuplicateClassification, classification.getName())); } return ValidationStatus.ok(); } } private static final class WeightsAreGreaterThan100Validator extends MultiValidator { private final Label label; private final Taxonomy taxonomy; private final List<IObservableValue> observables; private WeightsAreGreaterThan100Validator(Label label, Taxonomy taxonomy, List<IObservableValue> weightObservables) { this.label = label; this.taxonomy = taxonomy; this.observables = weightObservables; } @Override protected IStatus validate() { if (observables.isEmpty()) return ValidationStatus.ok(); int weights = 0; for (IObservableValue value : observables) weights += (Integer) value.getValue(); if (label != null) label.setText(Values.Weight.format(weights) + "%"); //$NON-NLS-1$ if (Classification.ONE_HUNDRED_PERCENT >= weights) return ValidationStatus.ok(); else return ValidationStatus.error(MessageFormat.format(Messages.EditWizardMasterDataMsgWeightNot100Percent, taxonomy.getName(), Values.Weight.format(weights))); } } private static final class NotNullValidator implements IValidator { @Override public IStatus validate(Object value) { return value != null ? ValidationStatus.ok() : ValidationStatus.error(Messages.EditWizardMasterDataMsgClassificationMissing); } } private static final class GreaterThanZeroValidator implements IValidator { @Override public IStatus validate(Object value) { int weight = (Integer) value; return weight > 0 ? ValidationStatus.ok() : ValidationStatus.error(Messages.EditWizardMasterDataMsgWeightEqualsZero); } } public static final String PAGE_NAME = "taxonomies"; //$NON-NLS-1$ private final EditSecurityModel model; private final BindingHelper bindings; private ScrolledComposite scrolledComposite; private Font boldFont; private List<ValidationStatusProvider> validators = new ArrayList<ValidationStatusProvider>(); public SecurityTaxonomyPage(EditSecurityModel model, BindingHelper bindings) { this.model = model; this.bindings = bindings; setTitle(Messages.LabelTaxonomies); } @Override public void createControl(Composite parent) { LocalResourceManager resources = new LocalResourceManager(JFaceResources.getResources(), parent); boldFont = resources.createFont(FontDescriptor.createFrom(parent.getFont()).setStyle(SWT.BOLD)); scrolledComposite = new ScrolledComposite(parent, SWT.H_SCROLL | SWT.V_SCROLL); setControl(scrolledComposite); Composite container = new Composite(scrolledComposite, SWT.NULL); GridLayoutFactory.fillDefaults().numColumns(2).margins(5, 5).applyTo(container); scrolledComposite.setContent(container); scrolledComposite.setExpandVertical(true); scrolledComposite.setExpandHorizontal(true); createTaxonomyPicker(container); scrolledComposite.addControlListener(new ControlAdapter() { public void controlResized(ControlEvent e) { scrolledComposite.setMinSize(container.computeSize(SWT.DEFAULT, SWT.DEFAULT)); } }); } private void createTaxonomySection(final Composite taxonomyPicker, final TaxonomyDesignation designation) { // label Label label = new Label(taxonomyPicker, SWT.NONE); label.setFont(boldFont); label.setText(designation.getTaxonomy().getName()); boolean isFirst = designation.equals(model.getDesignations().get(0)); GridDataFactory.fillDefaults().grab(true, false).span(2, 1).align(SWT.BEGINNING, SWT.CENTER) .indent(0, isFirst ? 0 : 20).applyTo(label); // drop-down selection block addBlock(taxonomyPicker, designation); // add button Link link = new Link(taxonomyPicker, SWT.UNDERLINE_LINK); link.setText(Messages.EditWizardMasterDataLinkNewCategory); GridDataFactory.fillDefaults().span(2, 1).indent(0, 5).align(SWT.BEGINNING, SWT.CENTER).applyTo(link); link.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { ClassificationLink link = new ClassificationLink(); link.setWeight(designation.getLinks().isEmpty() ? Classification.ONE_HUNDRED_PERCENT : 0); designation.getLinks().add(link); recreateTaxonomyPicker(taxonomyPicker); } }); } private void addBlock(final Composite taxonomyPicker, final TaxonomyDesignation designation) { Label sumOfWeights = null; final List<IObservableValue> weightObservables = new ArrayList<IObservableValue>(); final List<IObservableValue> classificationObservables = new ArrayList<IObservableValue>(); if (designation.getLinks().size() == 1 && designation.getLinks().get(0).getWeight() == Classification.ONE_HUNDRED_PERCENT) { addSimpleBlock(taxonomyPicker, designation, designation.getLinks().get(0), classificationObservables); } else if (!designation.getLinks().isEmpty()) { for (ClassificationLink link : designation.getLinks()) addFullBlock(taxonomyPicker, designation, link, weightObservables, classificationObservables); // add summary sumOfWeights = new Label(taxonomyPicker, SWT.NONE); sumOfWeights.setText(""); //$NON-NLS-1$ GridDataFactory.fillDefaults().span(2, 1).indent(0, 5).align(SWT.BEGINNING, SWT.CENTER) .applyTo(sumOfWeights); } setupWeightMultiValidator(sumOfWeights, designation, weightObservables); setupClassificationMultiValidator(designation, classificationObservables); } private void addSimpleBlock(Composite picker, TaxonomyDesignation designation, final ClassificationLink link, List<IObservableValue> classificationObservables) { Composite block = new Composite(picker, SWT.NONE); block.setBackground(picker.getBackground()); block.setData(link); GridDataFactory.fillDefaults().span(2, 1).applyTo(block); GridLayoutFactory.fillDefaults().numColumns(2).applyTo(block); addDropDown(block, designation, classificationObservables); addDeleteButton(block, designation, link); } private void addFullBlock(Composite picker, TaxonomyDesignation designation, final ClassificationLink link, List<IObservableValue> weightObservables, List<IObservableValue> classificationObservables) { Composite block = new Composite(picker, SWT.NONE); block.setData(link); GridDataFactory.fillDefaults().span(2, 1).applyTo(block); GridLayoutFactory.fillDefaults().numColumns(3).applyTo(block); addSpinner(block, link, weightObservables); addDropDown(block, designation, classificationObservables); addDeleteButton(block, designation, link); } private void recreateTaxonomyPicker(final Composite taxonomyPicker) { // bindings must be removed explicitly otherwise the model keeps 'old' // invalid bindings and error messages for (ValidationStatusProvider validator : validators) { if (validator instanceof Binding) bindings.getBindingContext().removeBinding((Binding) validator); else bindings.getBindingContext().removeValidationStatusProvider(validator); } validators.clear(); Composite parent = taxonomyPicker.getParent(); taxonomyPicker.dispose(); createTaxonomyPicker(parent); parent.layout(); } private void setupWeightMultiValidator(Label sumOfWeights, TaxonomyDesignation designation, final List<IObservableValue> weightObservables) { MultiValidator multiValidator = new WeightsAreGreaterThan100Validator(sumOfWeights, designation.getTaxonomy(), weightObservables); bindings.getBindingContext().addValidationStatusProvider(multiValidator); validators.add(multiValidator); for (int ii = 0; ii < weightObservables.size(); ii++) { IObservableValue observable = weightObservables.get(ii); ClassificationLink link = designation.getLinks().get(ii); UpdateValueStrategy strategy = new UpdateValueStrategy(); strategy.setAfterConvertValidator(new GreaterThanZeroValidator()); validators.add(bindings.getBindingContext().bindValue(multiValidator.observeValidatedValue(observable), BeanProperties.value("weight").observe(link), strategy, null)); //$NON-NLS-1$ } } private void setupClassificationMultiValidator(TaxonomyDesignation designation, final List<IObservableValue> classificationObservables) { MultiValidator multiValidator = new ClassificationNotTwiceValidator(classificationObservables); bindings.getBindingContext().addValidationStatusProvider(multiValidator); validators.add(multiValidator); for (int ii = 0; ii < classificationObservables.size(); ii++) { IObservableValue observable = classificationObservables.get(ii); ClassificationLink link = designation.getLinks().get(ii); UpdateValueStrategy strategy = new UpdateValueStrategy(); strategy.setAfterConvertValidator(new NotNullValidator()); validators.add(bindings.getBindingContext().bindValue(multiValidator.observeValidatedValue(observable), BeanProperties.value("classification").observe(link), strategy, null)); //$NON-NLS-1$ } } private void addDropDown(Composite block, TaxonomyDesignation designation, List<IObservableValue> classificationObservables) { final ComboViewer combo = new ComboViewer(block, SWT.READ_ONLY); combo.setContentProvider(ArrayContentProvider.getInstance()); combo.setLabelProvider(new LabelProvider() { @Override public String getText(Object element) { return ((Classification) element).getPathName(false, 80); } }); combo.setInput(designation.getElements()); GridDataFactory.fillDefaults().grab(false, false).applyTo(combo.getControl()); classificationObservables.add(ViewersObservables.observeSingleSelection(combo)); } private void addDeleteButton(final Composite block, final TaxonomyDesignation designation, final ClassificationLink link) { final Button deleteButton = new Button(block, SWT.PUSH); deleteButton.setImage(Images.REMOVE.image()); deleteButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { designation.getLinks().remove(link); recreateTaxonomyPicker(block.getParent()); } }); } private void addSpinner(Composite block, ClassificationLink link, List<IObservableValue> observables) { final Spinner spinner = new Spinner(block, SWT.BORDER); spinner.setDigits(2); spinner.setMinimum(0); spinner.setValues(link.getWeight(), 0, Classification.ONE_HUNDRED_PERCENT, 2, 100, 1000); GridDataFactory.fillDefaults().applyTo(spinner); observables.add(WidgetProperties.selection().observe(spinner)); } private void createTaxonomyPicker(Composite container) { final Composite taxonomyPicker = new Composite(container, SWT.NONE); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).span(2, 1).grab(true, false).applyTo(taxonomyPicker); GridLayoutFactory.fillDefaults().numColumns(2).margins(0, 0).spacing(0, 0).applyTo(taxonomyPicker); for (TaxonomyDesignation designation : model.getDesignations()) createTaxonomySection(taxonomyPicker, designation); scrolledComposite.setMinSize(container.computeSize(SWT.DEFAULT, SWT.DEFAULT)); } }