/******************************************************************************* * 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.Comparator; import java.util.List; import org.eclipse.core.databinding.Binding; import org.eclipse.core.databinding.DataBindingContext; 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.list.IListChangeListener; import org.eclipse.core.databinding.observable.list.IObservableList; import org.eclipse.core.databinding.observable.list.ListChangeEvent; import org.eclipse.core.databinding.observable.list.WritableList; import org.eclipse.core.databinding.observable.map.IObservableMap; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.observable.value.IValueChangeListener; import org.eclipse.core.databinding.observable.value.ValueChangeEvent; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.databinding.viewers.IViewerObservableList; import org.eclipse.jface.databinding.viewers.ObservableListContentProvider; import org.eclipse.jface.databinding.viewers.ObservableMapLabelProvider; import org.eclipse.jface.databinding.viewers.ViewersObservables; import org.eclipse.jface.viewers.AbstractListViewer; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.riena.internal.ui.ridgets.swt.OutputAwareValidator; import org.eclipse.riena.ui.common.ISortableByColumn; import org.eclipse.riena.ui.ridgets.IColumnFormatter; import org.eclipse.riena.ui.ridgets.IListRidget; import org.eclipse.riena.ui.ridgets.IMarkableRidget; import org.eclipse.riena.ui.ridgets.IRidget; import org.eclipse.riena.ui.ridgets.ITableFormatter; import org.eclipse.riena.ui.ridgets.listener.ClickEvent; /** * An abstract Ridget for lists that does not depend on the class org.eclipse.swt.widgets.List. May be used for Ridgets for custom lists. */ public abstract class AbstractListRidget extends AbstractSelectableIndexedRidget implements IListRidget { protected SelectionListener selectionTypeEnforcer; private DataBindingContext dbc; private Binding viewerSSB; /* * Binds the viewer's multiple selection to the multiple selection observable. This binding hsa to be disposed when the ridget is set to output-only, to * avoid updating the model. It has to be recreated when the ridget is set to not-output-only. */ private Binding viewerMSB; private Class<?> rowBeanClass; /* * Data we received in bindToModel(...). May change without our doing. */ private IObservableList modelObservables; /* * Data the viewer is bound to. It is updated from modelObservables on updateFromModel(). */ private IObservableList viewerObservables; private String renderingMethod; private ITableFormatter formatter; private boolean isSortedAscending; private int sortedColumn; private ViewerComparator comparator; public AbstractListRidget() { isSortedAscending = true; sortedColumn = -1; getSingleSelectionObservable().addValueChangeListener(new IValueChangeListener() { public void handleValueChange(final ValueChangeEvent event) { disableMandatoryMarkers(hasInput()); } }); getMultiSelectionObservable().addListChangeListener(new IListChangeListener() { public void handleListChange(final ListChangeEvent event) { disableMandatoryMarkers(hasInput()); } }); addPropertyChangeListener(IRidget.PROPERTY_ENABLED, new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { final boolean isEnabled = ((Boolean) evt.getNewValue()).booleanValue(); updateEnabled(isEnabled); } }); addPropertyChangeListener(IMarkableRidget.PROPERTY_OUTPUT_ONLY, new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { if (isOutputOnly()) { disposeMultipleSelectionBinding(); } else { createMultipleSelectionBinding(); } } }); } public void bindToModel(final IObservableList rowValues, final Class<? extends Object> rowClass, final String columnPropertyName) { Assert.isNotNull(columnPropertyName, "columnPropertyName"); //$NON-NLS-1$ final String[] columns = { columnPropertyName }; bindToModel(rowValues, rowClass, columns, null); } public void bindToModel(final Object listHolder, final String listPropertyName, final Class<? extends Object> rowClass, final String columnPropertyName) { Assert.isNotNull(columnPropertyName, "columnPropertyName"); //$NON-NLS-1$ final String[] columns = { columnPropertyName }; bindToModel(listHolder, listPropertyName, rowClass, columns, null); } /** * @since 2.0 */ public void bindToModel(final Object listHolder, final String listPropertyName) { bindToModel(listHolder, listPropertyName, Object.class, new String[] {}, null); } /** * {@inheritDoc} * <p> * Implementation note: the ListRidget ignores columnHeaders. * * @see #bindToModel(IObservableList, Class, String) */ public void bindToModel(final IObservableList rowValues, final Class<? extends Object> rowClass, final String[] columnPropertyNames, final String[] columnHeaders) { unbindUIControl(); rowBeanClass = rowClass; modelObservables = rowValues; viewerObservables = null; renderingMethod = columnPropertyNames.length > 0 ? columnPropertyNames[0] : null; bindUIControl(); } /** * {@inheritDoc} * <p> * Implementation note: the ListRidget ignores columnHeaders. * * @see #bindToModel(Object, String, Class, String) */ public void bindToModel(final Object listHolder, final String listPropertyName, final Class<? extends Object> rowClass, final String[] columnPropertyNames, final String[] columnHeaders) { IObservableList rowValues; if (AbstractSWTWidgetRidget.isBean(rowClass)) { rowValues = BeansObservables.observeList(listHolder, listPropertyName); } else { rowValues = PojoObservables.observeList(listHolder, listPropertyName); } bindToModel(rowValues, rowClass, columnPropertyNames, columnHeaders); } public IObservableList getObservableList() { return viewerObservables; } @Override public Object getOption(final int index) { if (getRowObservables() == null || index < 0 || index >= getOptionCount()) { throw new IllegalArgumentException("index: " + index); //$NON-NLS-1$ } final AbstractListViewer viewer = getViewer(); if (viewer != null) { return viewer.getElementAt(index); // sorted } return getRowObservables().get(index); // unsorted } @Override public int getSelectionIndex() { return getUIControl() == null ? -1 : getUIControlSelectionIndex(); } @Override public int[] getSelectionIndices() { return getUIControl() == null ? new int[0] : getUIControlSelectionIndices(); } public int getSortedColumn() { return comparator != null && sortedColumn == 0 ? 0 : -1; } public final boolean hasMoveableColumns() { return false; } @Override public int indexOfOption(final Object option) { if (getUIControl() != null) { final int optionCount = getUIControlItemCount(); for (int i = 0; i < optionCount; i++) { if (getViewer().getElementAt(i).equals(option)) { return i; } } } return -1; } public boolean isColumnSortable(final int columnIndex) { Assert.isLegal(columnIndex == 0, "columnIndex out of bounds (must be 0)"); //$NON-NLS-1$ return comparator != null; } @Override public boolean isDisableMandatoryMarker() { return hasInput(); } public boolean isSortedAscending() { return isSortedAscending; } public void refresh(final Object node) { final AbstractListViewer viewer = getViewer(); if (viewer != null) { viewer.refresh(node, true); } } /** * This method is not supported by this ridget. * * @throws UnsupportedOperationException * this is not supported by this ridget */ public void setColumnFormatter(final int columnIndex, final IColumnFormatter formatter) { throw new UnsupportedOperationException(); } /** * This method is not supported by this ridget. * * @throws UnsupportedOperationException * this is not supported by this ridget */ public final void setColumnSortable(final int columnIndex, final boolean sortable) { throw new UnsupportedOperationException(); } /** * This method is not supported by this ridget. * * @throws UnsupportedOperationException * this is not supported by this ridget * @since 4.0 */ public void clearColumnFormatters() { throw new UnsupportedOperationException(); } /** * This method is not supported by this ridget. * * @throws UnsupportedOperationException * this is not supported by this ridget */ public final void setColumnWidths(final Object[] widths) { throw new UnsupportedOperationException(); } public void setComparator(final int columnIndex, final Comparator<?> comparator) { Assert.isLegal(columnIndex == 0, "columnIndex out of bounds (must be 0)"); //$NON-NLS-1$ if (comparator != null) { final SortableComparator sortableComparator = new SortableComparator(this, comparator); this.comparator = new ViewerComparator(sortableComparator); } else { this.comparator = null; } updateComparator(); } /** * This method is not supported by this ridget. * * @throws UnsupportedOperationException * this is not supported by this ridget */ public final void setMoveableColumns(final boolean moveableColumns) { throw new UnsupportedOperationException("not implemented"); //$NON-NLS-1$ } public void setSortedAscending(final boolean ascending) { if (ascending != isSortedAscending) { final boolean oldSortedAscending = isSortedAscending; isSortedAscending = ascending; if (hasViewer()) { refreshViewer(); } firePropertyChange(ISortableByColumn.PROPERTY_SORT_ASCENDING, oldSortedAscending, isSortedAscending); } } public final void setSortedColumn(final int columnIndex) { final String msg = "columnIndex out of range (-1 - 0): " + columnIndex; //$NON-NLS-1$ Assert.isLegal(columnIndex >= -1 && columnIndex <= 0, msg); if (sortedColumn != columnIndex) { final int oldSortedColumn = sortedColumn; sortedColumn = columnIndex; updateComparator(); firePropertyChange(ISortableByColumn.PROPERTY_SORTED_COLUMN, oldSortedColumn, sortedColumn); } } @SuppressWarnings("unchecked") @Override public void updateFromModel() { super.updateFromModel(); if (modelObservables != null) { final List<Object> copy = new ArrayList<Object>(modelObservables); viewerObservables = new WritableList(copy, rowBeanClass); } if (viewerObservables != null) { if (hasViewer()) { final AbstractListViewer viewer = getViewer(); viewer.getControl().setRedraw(false); // prevent flicker during // update final StructuredSelection currentSelection = new StructuredSelection(getSelection()); try { configureViewer(viewer); } finally { viewer.setSelection(currentSelection); viewer.getControl().setRedraw(true); } } else { refreshSelection(); } } } protected void configureViewer(final AbstractListViewer viewer) { final ObservableListContentProvider viewerCP = new ObservableListContentProvider(); final String[] propertyNames = new String[] { renderingMethod }; IObservableMap[] attributeMap = null; // if renderingMethod is null, toString-Method will be used in ListLabelProvider if (null != renderingMethod) { if (AbstractSWTWidgetRidget.isBean(rowBeanClass)) { attributeMap = BeansObservables.observeMaps(viewerCP.getKnownElements(), rowBeanClass, propertyNames); } else { attributeMap = PojoObservables.observeMaps(viewerCP.getKnownElements(), rowBeanClass, propertyNames); } } viewer.setLabelProvider(new ListLabelProvider(attributeMap)); viewer.setContentProvider(viewerCP); viewer.setInput(viewerObservables); } protected void createSelectionBindings() { dbc = new DataBindingContext(); // viewer to single selection binding final IObservableValue viewerSelection = ViewersObservables.observeSingleSelection(getViewer()); viewerSSB = dbc.bindValue(viewerSelection, getSingleSelectionObservable(), new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE) .setAfterGetValidator(new OutputAwareValidator(this)), new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE)); // viewer to multiple selection binding viewerMSB = null; if (!isOutputOnly()) { createMultipleSelectionBinding(); } } @Override protected java.util.List<?> getRowObservables() { return viewerObservables; } protected abstract int getUIControlItemCount(); protected abstract int getUIControlSelectionIndex(); protected abstract int[] getUIControlSelectionIndices(); // helping methods // //////////////// protected abstract AbstractListViewer getViewer(); protected void disposeSelectionBindings() { if (viewerSSB != null && !viewerSSB.isDisposed()) { viewerSSB.dispose(); } disposeMultipleSelectionBinding(); } protected boolean hasViewer() { return getViewer() != null; } protected boolean hasViewerModel() { return viewerObservables != null; } protected void refreshViewer() { getViewer().refresh(); } /** * @see org.eclipse.riena.ui.ridgets.swt.AbstractSWTWidgetRidget#unbindUIControl() */ @Override protected void unbindUIControl() { super.unbindUIControl(); if (dbc != null) { disposeSelectionBindings(); dbc.dispose(); dbc = null; } } protected void updateComparator() { if (hasViewer()) { if (sortedColumn == 0) { getViewer().setComparator(this.comparator); } else { getViewer().setComparator(null); } } } /** * @since 4.0 */ @Override protected ClickEvent createClickEvent(final MouseEvent e) { Object rowData; final int index = getUIControlSelectionIndex(); if (index > -1) { rowData = getViewer().getElementAt(index); } else { rowData = null; } return new ClickEvent(AbstractListRidget.this, e.button, 0, rowData); } /** * {@inheritDoc} * * @throws UnsupportedOperationException * is always throw because the ListRidget doesn't support editing. * * @since 4.0 */ public void setColumnEditable(final int columnIndex, final boolean editable) { throw new UnsupportedOperationException("not supported"); //$NON-NLS-1$ } /** * {@inheritDoc} * * @throws UnsupportedOperationException * this is not supported by this ridget * @since 5.0 * */ public void setTableFormatter(final ITableFormatter formatter) { throw new UnsupportedOperationException("not supported"); //$NON-NLS-1$ } protected abstract void updateEnabled(boolean isEnabled); // helping methods // //////////////// private void createMultipleSelectionBinding() { if (viewerMSB == null && dbc != null && hasViewer()) { final StructuredSelection currentSelection = new StructuredSelection(getSelection()); final IViewerObservableList viewerSelections = ViewersObservables.observeMultiSelection(getViewer()); viewerMSB = dbc.bindList(viewerSelections, getMultiSelectionObservable(), new UpdateListStrategy(UpdateListStrategy.POLICY_UPDATE), new UpdateListStrategy(UpdateListStrategy.POLICY_UPDATE)); getViewer().setSelection(currentSelection); } } private void disposeMultipleSelectionBinding() { if (viewerMSB != null) { // implies dbc != null viewerMSB.dispose(); dbc.removeBinding(viewerMSB); viewerMSB = null; } } private boolean hasInput() { return !getSelection().isEmpty(); } // helping classes // //////////////// /** * Generates the labels (i.e. strings) shown in the list. */ private final class ListLabelProvider extends ObservableMapLabelProvider { private final boolean useToString; public ListLabelProvider(final IObservableMap[] attributeMap) { super(null == attributeMap ? new IObservableMap[] {} : attributeMap); useToString = attributeMap == null; } @Override public String getColumnText(final Object element, final int columnIndex) { String result; if (MarkerSupport.isHideDisabledRidgetContent() && !isEnabled()) { result = ""; //$NON-NLS-1$ } else { result = useToString ? toString(element) : super.getColumnText(element, columnIndex); } return result; } private String toString(final Object element) { if (element == null) { throw new NullPointerException("Row-element in ListRidget is null"); //$NON-NLS-1$ } final String result = element.toString(); if (result == null) { throw new NullPointerException("Row-element.toString() returned null"); //$NON-NLS-1$ } return result; } } }