/******************************************************************************* * Copyright (c) 2007, 2014 compeople AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * compeople AG - initial API and implementation *******************************************************************************/ package org.eclipse.riena.ui.ridgets.swt; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import org.eclipse.core.databinding.Binding; import org.eclipse.core.databinding.BindingException; import org.eclipse.core.databinding.DataBindingContext; import org.eclipse.core.databinding.ListBinding; import org.eclipse.core.databinding.UpdateListStrategy; import org.eclipse.core.databinding.UpdateValueStrategy; import org.eclipse.core.databinding.beans.BeansObservables; import org.eclipse.core.databinding.beans.PojoObservables; import org.eclipse.core.databinding.observable.Diffs; import org.eclipse.core.databinding.observable.list.IObservableList; import org.eclipse.core.databinding.observable.list.ListDiff; import org.eclipse.core.databinding.observable.list.ListDiffEntry; import org.eclipse.core.databinding.observable.list.WritableList; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.observable.value.ValueDiff; import org.eclipse.core.databinding.observable.value.WritableValue; import org.eclipse.core.runtime.Assert; import org.eclipse.riena.core.util.ListenerList; import org.eclipse.riena.ui.ridgets.IFilterableContentRidget; import org.eclipse.riena.ui.ridgets.IRidgetContentFilter; import org.eclipse.riena.ui.ridgets.IRidgetContentFilterHolder; import org.eclipse.riena.ui.ridgets.ISelectableRidget; import org.eclipse.riena.ui.ridgets.listener.ISelectionListener; import org.eclipse.riena.ui.ridgets.listener.SelectionEvent; /** * Default implementation of an {@link ISelectableRidget}. This ridget than can observe single and multiple selection of a widget and bind the selection state * to model elements. */ public abstract class AbstractSelectableRidget extends AbstractSWTRidget implements ISelectableRidget, IFilterableContentRidget { /** The selected option (single-selection). */ private final WritableValue singleSelectionObservable; /** A list of selected options (multiple-selection). */ private final WritableList multiSelectionObservable; /** Binds the singleSelectionObservable to some model. */ private Binding singleSelectionBinding; /** Binds the multiSelectionObservable to some model. */ private ListBinding multiSelectionBinding; /** The selection type. */ private SelectionType selectionType = SelectionType.SINGLE; /** A list of selection listeners. */ private ListenerList<ISelectionListener> selectionListeners; public AbstractSelectableRidget() { singleSelectionObservable = new SingleSelectionObservable(); multiSelectionObservable = new MultiSelectionObservable(); } /** * {@inheritDoc} * * @since 2.0 */ public void addSelectionListener(final ISelectionListener selectionListener) { Assert.isNotNull(selectionListener, "selectionListener is null"); //$NON-NLS-1$ if (selectionListeners == null) { selectionListeners = new ListenerList<ISelectionListener>(ISelectionListener.class); addPropertyChangeListener(ISelectableRidget.PROPERTY_SELECTION, new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { notifySelectionListeners((List<?>) evt.getOldValue(), (List<?>) evt.getNewValue()); } }); } selectionListeners.add(selectionListener); } public final void bindMultiSelectionToModel(final IObservableList observableList) { if (multiSelectionBinding != null) { multiSelectionBinding.dispose(); } final DataBindingContext dbc = new DataBindingContext(); multiSelectionBinding = (ListBinding) dbc.bindList(multiSelectionObservable, observableList, new UpdateListStrategy(UpdateListStrategy.POLICY_UPDATE), new UpdateListStrategy(UpdateListStrategy.POLICY_ON_REQUEST)); } public final void bindMultiSelectionToModel(final Object selectionHolder, final String selectionPropertyName) { IObservableList observableList; if (AbstractSWTRidget.isBean(selectionHolder.getClass())) { observableList = BeansObservables.observeList(selectionHolder, selectionPropertyName); } else { observableList = PojoObservables.observeList(selectionHolder, selectionPropertyName); } bindMultiSelectionToModel(observableList); } public final void bindSingleSelectionToModel(final IObservableValue selectionValue) { if (singleSelectionBinding != null) { singleSelectionBinding.dispose(); } final DataBindingContext dbc = new DataBindingContext(); singleSelectionBinding = dbc.bindValue(singleSelectionObservable, selectionValue, new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE), new UpdateValueStrategy(UpdateValueStrategy.POLICY_ON_REQUEST)); } public final void bindSingleSelectionToModel(final Object selectionHolder, final String selectionPropertyName) { IObservableValue observableValue; if (AbstractSWTRidget.isBean(selectionHolder.getClass())) { observableValue = PojoObservables.observeValue(selectionHolder, selectionPropertyName); } else { observableValue = PojoObservables.observeValue(selectionHolder, selectionPropertyName); } bindSingleSelectionToModel(observableValue); } public void clearSelection() { singleSelectionObservable.setValue(null); multiSelectionObservable.clear(); } public boolean containsOption(final Object option) { if (getRowObservables() == null) { return false; } return getRowObservables().contains(option); } public final IObservableList getMultiSelectionObservable() { return multiSelectionObservable; } public final List<Object> getSelection() { final List<Object> result = new ArrayList<Object>(); if (SelectionType.SINGLE.equals(selectionType)) { if (singleSelectionObservable.getValue() != null) { result.add(singleSelectionObservable.getValue()); } } else if (SelectionType.MULTI.equals(selectionType)) { result.addAll(Arrays.asList(multiSelectionObservable.toArray())); } return result; } /** * @since 4.0 */ public <T> T getSingleSelection() { if (!hasSelection()) { return null; } Assert.isLegal(getSelectionType() == SelectionType.SINGLE, "A single selection is only provided if selection type is SelectionType.SINGLE. "); //$NON-NLS-1$ final List<Object> selection = getSelection(); if (selection.size() != 1) { return null; } return (T) selection.get(0); } /** * @since 4.0 */ @SuppressWarnings("unchecked") public <T> List<T> getMultiSelection() { if (!hasSelection()) { return null; } Assert.isLegal(getSelectionType() == SelectionType.MULTI, "A multi selection is only provided if selection type is SelectionType.MULTI. "); //$NON-NLS-1$ final List<Object> selection = getSelection(); if (selection.size() < 0) { return null; } return (List<T>) selection; } /** * @since 4.0 */ public final SelectionType getSelectionType() { return selectionType; } /** * @since 4.0 */ public boolean hasSelection() { final List<Object> selection = getSelection(); if (null == selection || selection.isEmpty()) { return false; } return true; } public final IObservableValue getSingleSelectionObservable() { return singleSelectionObservable; } /** * {@inheritDoc} * * @since 2.0 */ public void removeSelectionListener(final ISelectionListener selectionListener) { if (selectionListeners != null) { selectionListeners.remove(selectionListener); } } public void setSelection(final List<?> newSelection) { assertIsBoundToModel(); final List<?> knownElements = getKnownElements(newSelection); if (SelectionType.SINGLE.equals(selectionType)) { final Object value = knownElements.size() > 0 ? knownElements.get(0) : null; singleSelectionObservable.setValue(value); } else if (SelectionType.MULTI.equals(selectionType)) { final ListDiff diff = Diffs.computeListDiff(multiSelectionObservable, knownElements); if (diff.getDifferences().length > 0) { multiSelectionObservable.clear(); multiSelectionObservable.addAll(knownElements); } } } public final void setSelection(final Object newSelection) { assertIsBoundToModel(); final List<Object> list = Arrays.asList(new Object[] { newSelection }); setSelection(list); } public void setSelectionType(final SelectionType selectionType) { Assert.isNotNull(selectionType, "selectionType cannot be null"); //$NON-NLS-1$ if (SelectionType.NONE.equals(selectionType)) { throw new IllegalArgumentException("SelectionType.NONE is not supported by the UI-control"); //$NON-NLS-1$ } if (!this.selectionType.equals(selectionType)) { this.selectionType = selectionType; } } public final void updateMultiSelectionFromModel() { if (multiSelectionBinding != null) { multiSelectionBinding.updateModelToTarget(); } } public final void updateSingleSelectionFromModel() { if (singleSelectionBinding != null) { singleSelectionBinding.updateModelToTarget(); } } /** * {@inheritDoc} * * @since 4.0 */ public void addFilter(final IRidgetContentFilter filter) { final IRidgetContentFilterHolder<?> filterHolder = getFilterHolder(); if (filterHolder == null) { throw new UnsupportedOperationException("This ridget type does not support filtering: " + getClass().getSimpleName()); //$NON-NLS-1$ } filterHolder.add(filter); } /** * {@inheritDoc} * * @since 4.0 */ public void removeFilter(final IRidgetContentFilter filter) { final IRidgetContentFilterHolder<?> filterHolder = getFilterHolder(); if (filterHolder != null) { filterHolder.remove(filter); } } // protected methods // ////////////////// /** * Retrieves the {@link IRidgetContentFilterHolder} for this ridget. * * @return the filter holder for this ridget * @since 4.0 */ protected abstract IRidgetContentFilterHolder<?> getFilterHolder(); /** * Return an observable list of objects which can be selected through this ridget. * * @return a List instance or null, if the ridget has not been bound to a model */ abstract protected List<?> getRowObservables(); /** * Throws an exception if no model observables are available (i.e. getRowObservables() == null). */ protected final void assertIsBoundToModel() { if (getRowObservables() == null) { throw new BindingException("ridget not bound to model"); //$NON-NLS-1$ } } /** * Updates the current selection to ensure that all selected items are still available in the ridget's model. * <p> * Subclasses should call this from their {@link #updateFromModel()} method. * <p> * Implementation note: the method computes the subset of the current selection that is available. If the subset is smaller that the current selection, it * will be applied and become the new current selection. * * @since 2.0 */ protected final void refreshSelection() { final List<?> rowObservables = getRowObservables(); if (rowObservables != null) { boolean doUpdate = false; final List<Object> oldSelection = getSelection(); final List<Object> newSelection = new ArrayList<Object>(); for (final Object candidate : oldSelection) { if (!rowObservables.contains(candidate)) { doUpdate = true; } else { newSelection.add(candidate); } } if (doUpdate) { setSelection(newSelection); } } } // helping methods // //////////////// private List<Object> getKnownElements(final List<?> elements) { final List<Object> result = new ArrayList<Object>(); final Collection<?> rowObservables = getRowObservables(); for (final Object element : elements) { if (rowObservables.contains(element)) { result.add(element); } } return result; } private void notifySelectionListeners(final List<?> oldSelectionList, final List<?> newSelectionList) { if (selectionListeners != null) { final SelectionEvent event = new SelectionEvent(this, oldSelectionList, newSelectionList); for (final ISelectionListener listener : selectionListeners.getListeners()) { listener.ridgetSelected(event); } } } // helping classes // //////////////// /** * Observable value for single selection. This class is used by this ridget to monitor and maintain the selection state for single selection and fire * appropriate events. */ private final class SingleSelectionObservable extends WritableValue { SingleSelectionObservable() { super(null, Object.class); } @Override protected void fireValueChange(final ValueDiff diff) { super.fireValueChange(diff); final Object newValue = diff.getNewValue(); final Object oldValue = diff.getOldValue(); if (oldValue != newValue && getSelectionType() == SelectionType.SINGLE) { final String key = ISelectableRidget.PROPERTY_SELECTION; AbstractSelectableRidget.this.propertyChangeSupport.firePropertyChange(key, toList(oldValue), toList(newValue)); } } private List<?> toList(final Object value) { return value == null ? Collections.EMPTY_LIST : Arrays.asList(new Object[] { value }); } } /** * Observable list for multiple selection. This class is used by this ridget to monitor and maintain the selection state for multiple selection and fire * appropriate events. */ private final class MultiSelectionObservable extends WritableList { MultiSelectionObservable() { super(new ArrayList<Object>(), Object.class); } /** * Only the MultiSelectionObservable is firing selection property change events to avoid duplicate events (bug 268897) */ @Override protected void fireListChange(final ListDiff diff) { super.fireListChange(diff); if (getSelectionType() == SelectionType.MULTI) { final List<Object> newSelection = Arrays.asList(toArray()); final List<Object> oldSelection = computeOldSelection(diff, newSelection); final String key = ISelectableRidget.PROPERTY_SELECTION; AbstractSelectableRidget.this.propertyChangeSupport.firePropertyChange(key, oldSelection, newSelection); } } private List<Object> computeOldSelection(final ListDiff diff, final List<Object> newSelection) { final List<Object> oldSelection = new ArrayList<Object>(); oldSelection.addAll(newSelection); for (final ListDiffEntry entry : diff.getDifferences()) { final Object element = entry.getElement(); if (entry.isAddition()) { oldSelection.remove(element); } else { oldSelection.add(element); } } return oldSelection; } } }