/******************************************************************************* * 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.internal.ui.ridgets.swt; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyDescriptor; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.osgi.service.log.LogService; 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.ListDiffEntry; 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.runtime.Assert; import org.eclipse.equinox.log.Logger; import org.eclipse.jface.databinding.viewers.IViewerObservableList; import org.eclipse.jface.databinding.viewers.ObservableListContentProvider; import org.eclipse.jface.databinding.viewers.ViewersObservables; import org.eclipse.jface.viewers.AbstractTableViewer; import org.eclipse.jface.viewers.ColumnLayoutData; import org.eclipse.jface.viewers.ColumnPixelData; import org.eclipse.jface.viewers.ColumnWeightData; import org.eclipse.jface.viewers.EditingSupport; import org.eclipse.jface.viewers.IBaseLabelProvider; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.ViewerColumn; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Item; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Scrollable; import org.eclipse.swt.widgets.Widget; import org.eclipse.riena.core.Log4r; import org.eclipse.riena.ui.common.ISortableByColumn; import org.eclipse.riena.ui.core.marker.RowErrorMessageMarker; import org.eclipse.riena.ui.ridgets.IColumnFormatter; import org.eclipse.riena.ui.ridgets.IMarkableRidget; import org.eclipse.riena.ui.ridgets.ISelectableRidget; import org.eclipse.riena.ui.ridgets.ITableFormatter; import org.eclipse.riena.ui.ridgets.ITableRidget; import org.eclipse.riena.ui.ridgets.swt.AbstractSWTWidgetRidget; import org.eclipse.riena.ui.ridgets.swt.AbstractSelectableIndexedRidget; import org.eclipse.riena.ui.ridgets.swt.ColumnFormatter; /** * Abstract Ridget of a table widget. */ public abstract class AbstractTableRidget extends AbstractSelectableIndexedRidget implements ITableRidget { private final static Logger LOGGER = Log4r.getLogger(AbstractTableRidget.class); private final static String TABLE_VIEWER_DATA_KEY = "TBL.VIEWER"; //$NON-NLS-1$ private AbstractTableViewer viewer; private ColumnLayoutData[] columnWidths; private ITableTreeWrapper tableWrapper; protected final SelectionListener selectionTypeEnforcer; protected SelectionListener sortListener; protected String[] columnHeaders; protected DataBindingContext dbc; protected String[] renderingMethods; private boolean viewConfigured; private boolean nativeToolTip = true; private boolean isSortedAscending; private int sortedColumn; private ITableFormatter tableFormatter; private final Map<Integer, Boolean> sortableColumnsMap; private final Map<Integer, Boolean> editableColumnsMap; private final Map<Integer, Comparator<?>> comparatorMap; private final Map<Integer, IColumnFormatter> formatterMap; private final TableKeyListener keyListener; /* * Binds the viewer's multiple selection to the multiple selection observable. This binding has 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; /* * 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(). */ protected IObservableList viewerObservables; private Class<?> rowClass; private boolean moveableColumns; private StructuredViewerFilterHolder filterHolder; /** * @return the viewConfigured */ protected boolean isViewConfigured() { return viewConfigured; } public AbstractTableRidget() { isSortedAscending = true; sortedColumn = -1; // maps formatterMap = new HashMap<Integer, IColumnFormatter>(); sortableColumnsMap = new HashMap<Integer, Boolean>(); editableColumnsMap = new HashMap<Integer, Boolean>(); comparatorMap = new HashMap<Integer, Comparator<?>>(); // listener keyListener = new TableKeyListener(); selectionTypeEnforcer = new SelectionTypeEnforcer(); addPropertyChangeListener(IMarkableRidget.PROPERTY_OUTPUT_ONLY, new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { if (isOutputOnly()) { disposeMultipleSelectionBinding(); } else { createMultipleSelectionBinding(); } } }); } /** * Returns the number of columns in the table widget. * * @return number of columns; -1 if the number of columns can't be determined */ public int getColumnCount() { if (tableWrapper == null) { return -1; } return tableWrapper.getColumnCount(); } protected int getExpectedColumnCount() { if (renderingMethods == null) { return 0; } return renderingMethods.length; } /** * Returns the number of items contained in the table widget. * * @return number of items */ private int getItemCount() { if (tableWrapper == null) { return -1; } return tableWrapper.getItemCount(); } /** * Returns the item at the given point in the table or null if no such item exists. The point is in the coordinate system of the table. * * @param point * the point used to locate the item * @return the item at the given point */ protected final Item getItem(final Point point) { if (tableWrapper == null) { return null; } return tableWrapper.getItem(point); } /** * Returns the number of selected items contained in the table widget. * * @return the number of selected items */ protected abstract int getUiSelectionCount(); /** * Sets the table widget's selection to the given item. * * @param item * the item to select */ protected abstract void setUiSelection(Widget item); // protected abstract void setUiSelection(int selectionIndex); // protected abstract int getUiSelectionIndex(); /** * Creates columns for every given column property. (see: {@link #bindToModel(Object, String, Class, String[], String[])}). */ protected abstract void applyColumns(); /** * Sets the text of the column headers. */ protected abstract void applyTableColumnHeaders(); /** * Sets whether the columns are movable or not. */ protected abstract void applyColumnsMovable(); /** * Sets the comparator for the column that is sorted. Form the other columns the comparator is removed. * * @param comparatorMap * map with all comparators of every column that has a comparator. */ protected abstract void applyComparator(final Map<Integer, Comparator<?>> comparatorMap); /** * Adds a support for tool tips to display tool tips for single cell, error marker texts etc.. */ protected abstract void updateToolTipSupport(); /** * Creates a JFace viewer for the table control. * * @return table viewer */ protected abstract AbstractTableViewer createTableViewer(); protected abstract ITableTreeWrapper createTableWrapper(); protected abstract int getColumnStyle(int columnIndex); /** * {@inheritDoc} * <p> * Implementation note: if the array is non-null, its elements must be {@link ColumnPixelData} or {@link ColumnWeightData} instances. * * @throws RuntimeException * if an unsupported array element is encountered */ public void setColumnWidths(final Object[] widths) { columnWidths = ColumnUtils.copyWidths(widths); applyColumnWidths(); } protected final void applyColumnWidths() { if (tableWrapper == null) { return; } ColumnUtils.applyColumnWidths(tableWrapper, columnWidths); } protected void applyEditingSupport(final ViewerColumn viewerColumn, final int columnIndex) { final Boolean editable = editableColumnsMap.get(columnIndex); EditingSupport editingSupport = null; if (Boolean.TRUE.equals(editable)) { final PropertyDescriptor property = getPropertyDescriptor(renderingMethods[columnIndex]); final int columnStyle = getColumnStyle(columnIndex); // editingSupport = new InlineEditingSupport0(this, dbc, property, columnStyle); editingSupport = new TableRidgetEditingSupport(this, property, columnStyle); } viewerColumn.setEditingSupport(editingSupport); } protected PropertyDescriptor getPropertyDescriptor(final String propertyName) { PropertyDescriptor[] descriptors; try { descriptors = Introspector.getBeanInfo(rowClass).getPropertyDescriptors(); for (final PropertyDescriptor descriptor : descriptors) { if (descriptor.getName().equals(propertyName)) { return descriptor; } } } catch (final IntrospectionException e) { final String msg = "Could not detect descriptor of the property " + propertyName; //$NON-NLS-1$ LOGGER.log(LogService.LOG_ERROR, msg, e); } return null; } protected PropertyDescriptor getPropertyDescriptor(final int columnIndex) { checkColumnRange(columnIndex); return getPropertyDescriptor(renderingMethods[columnIndex]); } protected void checkColumnRange(final int columnIndex) { final int range = getColumnCount(); if (range != -1) { final String msg = "columnIndex out of range (0 - " + range + " ): " + columnIndex; //$NON-NLS-1$ //$NON-NLS-2$ Assert.isLegal(-1 < columnIndex, msg); Assert.isLegal(columnIndex < range, msg); } } public void setColumnFormatter(final int columnIndex, final IColumnFormatter formatter) { checkColumnRange(columnIndex); if (formatter != null) { Assert.isLegal(formatter instanceof ColumnFormatter, "formatter must sublass ColumnFormatter"); //$NON-NLS-1$ } final Integer key = Integer.valueOf(columnIndex); formatterMap.put(key, formatter); } public void setTableFormatter(final ITableFormatter formatter) { tableFormatter = formatter; } public void clearColumnFormatters() { formatterMap.clear(); } protected IColumnFormatter[] getColumnFormatters(final int numColumns) { Assert.isLegal(numColumns >= 0); final IColumnFormatter[] result = new IColumnFormatter[numColumns]; for (int i = 0; i < numColumns; i++) { final IColumnFormatter columnFormatter = formatterMap.get(Integer.valueOf(i)); if (columnFormatter != null) { result[i] = columnFormatter; } } return result; } private boolean isViewerConfigured() { return (viewer.getLabelProvider() instanceof TableRidgetLabelProvider) && viewConfigured; } @Override public int indexOfOption(final Object option) { final int optionCount = getItemCount(); if (optionCount > 0) { // implies viewer != null for (int i = 0; i < optionCount; i++) { if (viewer.getElementAt(i).equals(option)) { return i; } } } return -1; } public void refresh(final Object element) { if (viewer != null) { viewer.refresh(element, true); } } @Override protected void bindUIControl() { final Control control = getUIControl(); if (control != null) { dbc = new DataBindingContext(); tableWrapper = createTableWrapper(); // try to reuse the table viewer viewer = (AbstractTableViewer) control.getData(TABLE_VIEWER_DATA_KEY); if ((viewer == null) || (viewer.getControl() != control)) { viewer = createTableViewer(); control.setData(TABLE_VIEWER_DATA_KEY, viewer); } configureControl(); if (getObservableList() != null) { configureViewer(viewer); } // viewer to single selection binding final IObservableValue viewerSelection = ViewersObservables.observeSingleSelection(viewer); dbc.bindValue(viewerSelection, getSingleSelectionObservable(), new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE).setAfterGetValidator(new OutputAwareValidator(this)), new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE)); control.addKeyListener(keyListener); updateToolTipSupport(); // viewer to to multi-selection binding viewerMSB = null; if (!isOutputOnly()) { createMultipleSelectionBinding(); } } getFilterHolder().activate(viewer); } @Override protected void unbindUIControl() { super.unbindUIControl(); getFilterHolder().deactivate(viewer); disposeMultipleSelectionBinding(); if (dbc != null) { dbc.dispose(); dbc = null; } final Control control = getUIControl(); if (control != null) { control.removeKeyListener(keyListener); } viewConfigured = false; viewer = null; tableWrapper = null; } @SuppressWarnings("unchecked") @Override public void updateFromModel() { super.updateFromModel(); if (modelObservables != null) { final List<Object> copy = new ArrayList<Object>(modelObservables); viewerObservables = new WritableList(copy, rowClass); } if (viewer != null) { if (!isViewerConfigured()) { configureControl(); configureViewer(viewer); } else { refreshViewer(viewer); } } else { refreshSelection(); } } private void configureControl() { if (renderingMethods != null) { applyColumns(); } applyColumnsMovable(); applyTableColumnHeaders(); } protected void configureViewer(final AbstractTableViewer viewer) { final ObservableListContentProvider viewerCP = new ObservableListContentProvider(); final TableRidgetLabelProvider labelProvider = createLabelProvider(viewerCP); viewer.setLabelProvider(labelProvider); viewer.setContentProvider(viewerCP); configureLableProvider(labelProvider, viewer); viewer.setInput(viewerObservables); applyComparator(comparatorMap); viewConfigured = true; } private void refreshViewer(final AbstractTableViewer viewer) { viewer.getControl().setRedraw(false); // prevent flicker during update final boolean scrollBarVisibleBeforeRefresh = isVerticalScrollBarVisible(viewer.getControl()); final StructuredSelection currentSelection = new StructuredSelection(getSelection()); try { configureLableProvider(viewer.getLabelProvider(), viewer); viewer.setInput(viewerObservables); } finally { viewer.setSelection(currentSelection); viewer.getControl().setRedraw(true); layoutIfScrollBarVisibilityChanged(viewer.getControl(), scrollBarVisibleBeforeRefresh); } } protected void configureLableProvider(final IBaseLabelProvider labelProvider, final AbstractTableViewer viewer) { if (labelProvider instanceof TableRidgetLabelProvider) { final TableRidgetLabelProvider tableLabelProvider = (TableRidgetLabelProvider) labelProvider; final IColumnFormatter[] formatters = getColumnFormatters(tableLabelProvider.getColumnCount()); tableLabelProvider.setFormatters(formatters); tableLabelProvider.setTableFormatter(tableFormatter); } } @Override protected StructuredViewerFilterHolder getFilterHolder() { if (filterHolder == null) { filterHolder = new StructuredViewerFilterHolder(); } return filterHolder; } /** * Creates the label provider of this table. * * @param viewerCP * @return label provider */ private TableRidgetLabelProvider createLabelProvider(final ObservableListContentProvider viewerCP) { IObservableMap[] attrMap; if (AbstractSWTWidgetRidget.isBean(rowClass)) { attrMap = BeansObservables.observeMaps(viewerCP.getKnownElements(), rowClass, renderingMethods); } else { attrMap = PojoObservables.observeMaps(viewerCP.getKnownElements(), rowClass, renderingMethods); } final IColumnFormatter[] formatters = getColumnFormatters(attrMap.length); final TableRidgetLabelProvider labelProvider = new TableRidgetLabelProvider(attrMap, formatters); viewerObservables.addListChangeListener(new IListChangeListener() { public void handleListChange(final ListChangeEvent event) { if ((event.diff != null) && (!event.diff.isEmpty())) { for (final ListDiffEntry diffEntry : event.diff.getDifferences()) { if (!diffEntry.isAddition()) { // an element of the table was removed, // dispose the according image (stored inside the label provider) final Object deletedElement = diffEntry.getElement(); if (deletedElement != null) { labelProvider.disposeImageOfElement(deletedElement); } } } } } }); return labelProvider; } /** * Checks if the table scroll bar visibility changed and triggers a layout() if needed. * <p> * When using TableLayout, tables do not consider the presence of a vertical scroll bar and the space it needs in case that the table needs a vertical * scroll bar, also a horizontal bar will appear (swt/jface problem). This method works around that. */ protected void layoutIfScrollBarVisibilityChanged(final Control control, final boolean barVisibilityBefore) { if (barVisibilityBefore != isVerticalScrollBarVisible(control)) { if (control.getParent().getChildren().length == 1) { control.getParent().layout(true, true); } } } /** * @return <code>true</code> if the given control is {@link Scrollable} and the vertical scroll bar is visible */ protected boolean isVerticalScrollBarVisible(final Control control) { if (control instanceof Scrollable) { final ScrollBar sb = ((Scrollable) control).getVerticalBar(); return sb != null && sb.isVisible(); } return false; } public void bindToModel(final IObservableList rowObservables, final Class<? extends Object> aRowClass, final String[] columnPropertyNames, final String[] columnHeaders) { if (columnHeaders != null) { final String msg = "Mismatch between number of columnPropertyNames and columnHeaders"; //$NON-NLS-1$ Assert.isLegal(columnPropertyNames.length == columnHeaders.length, msg); } unbindUIControl(); rowClass = aRowClass; modelObservables = rowObservables; viewerObservables = null; renderingMethods = new String[columnPropertyNames.length]; System.arraycopy(columnPropertyNames, 0, renderingMethods, 0, renderingMethods.length); if (columnHeaders != null) { this.columnHeaders = new String[columnHeaders.length]; System.arraycopy(columnHeaders, 0, this.columnHeaders, 0, this.columnHeaders.length); } else { this.columnHeaders = null; } bindUIControl(); } 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); } @Override public Object getOption(final int index) { if (getRowObservables() == null || index < 0 || index >= getOptionCount()) { throw new IllegalArgumentException("index: " + index); //$NON-NLS-1$ } if (viewer != null) { return viewer.getElementAt(index); // sorted } return getRowObservables().get(index); // unsorted } @Override protected java.util.List<?> getRowObservables() { return viewerObservables; } public IObservableList getObservableList() { return viewerObservables; } public boolean hasMoveableColumns() { return moveableColumns; } public void setMoveableColumns(final boolean moveableColumns) { if (this.moveableColumns != moveableColumns) { this.moveableColumns = moveableColumns; applyColumnsMovable(); } } public boolean isNativeToolTip() { return nativeToolTip; } public void setNativeToolTip(final boolean nativeToolTip) { if (this.nativeToolTip != nativeToolTip) { this.nativeToolTip = nativeToolTip; updateToolTipSupport(); } } public void setComparator(final int columnIndex, final Comparator<?> compi) { checkColumnRange(columnIndex); final Integer key = Integer.valueOf(columnIndex); if (compi != null) { comparatorMap.put(key, compi); } else { comparatorMap.remove(key); } if (columnIndex == sortedColumn) { applyComparator(comparatorMap); } } public boolean isColumnSortable(final int columnIndex) { checkColumnRange(columnIndex); boolean result = false; final Integer key = Integer.valueOf(columnIndex); final Boolean sortable = sortableColumnsMap.get(columnIndex); if (!Boolean.FALSE.equals(sortable)) { result = comparatorMap.get(key) != null; } return result; } protected int getSortDirection() { final int direction = isSortedAscending() ? SWT.UP : SWT.DOWN; return direction; } public int getSortedColumn() { final boolean isSorted = sortedColumn != -1 && isColumnSortable(sortedColumn); return isSorted ? sortedColumn : -1; } public boolean isSortedAscending() { return getSortedColumn() != -1 && isSortedAscending; } public void setColumnSortable(final int columnIndex, final boolean sortable) { checkColumnRange(columnIndex); final Integer key = Integer.valueOf(columnIndex); final Boolean newValue = Boolean.valueOf(sortable); Boolean oldValue = sortableColumnsMap.put(key, newValue); if (oldValue == null) { oldValue = Boolean.TRUE; } if (!newValue.equals(oldValue)) { firePropertyChange(ISortableByColumn.PROPERTY_COLUMN_SORTABILITY, null, columnIndex); } } public void setColumnEditable(final int columnIndex, final boolean editable) { checkColumnRange(columnIndex); final Integer key = Integer.valueOf(columnIndex); final Boolean newValue = Boolean.valueOf(editable); Boolean oldValue = editableColumnsMap.put(key, newValue); if (oldValue == null) { oldValue = Boolean.TRUE; } } public void setSortedAscending(final boolean ascending) { if (isSortedAscending != ascending) { final boolean oldSortedAscending = isSortedAscending; isSortedAscending = ascending; applyComparator(comparatorMap); firePropertyChange(ISortableByColumn.PROPERTY_SORT_ASCENDING, oldSortedAscending, isSortedAscending); } } public void setSortedColumn(final int columnIndex) { if (columnIndex != -1) { checkColumnRange(columnIndex); } if (sortedColumn != columnIndex) { final int oldSortedColumn = sortedColumn; sortedColumn = columnIndex; applyComparator(comparatorMap); firePropertyChange(ISortableByColumn.PROPERTY_SORTED_COLUMN, oldSortedColumn, sortedColumn); } } protected AbstractTableViewer getTableViewer() { return viewer; } /** * Always returns true because mandatory markers do not make sense for this ridget. */ @Override public boolean isDisableMandatoryMarker() { return true; } protected boolean isErrorMarked(final Widget item) { final Object data = item.getData(); final Collection<RowErrorMessageMarker> markers = getMarkersOfType(RowErrorMessageMarker.class); for (final RowErrorMessageMarker marker : markers) { if (marker.getRowValue() == data) { return true; } } return false; } private void createMultipleSelectionBinding() { if (viewerMSB == null && dbc != null && viewer != null) { final StructuredSelection currentSelection = new StructuredSelection(getSelection()); final IViewerObservableList viewerSelections = ViewersObservables.observeMultiSelection(viewer); viewerMSB = dbc.bindList(viewerSelections, getMultiSelectionObservable(), new UpdateListStrategy(UpdateListStrategy.POLICY_UPDATE), new UpdateListStrategy(UpdateListStrategy.POLICY_UPDATE)); viewer.setSelection(currentSelection); } } private void disposeMultipleSelectionBinding() { if (viewerMSB != null) { viewerMSB.dispose(); if (dbc != null) { dbc.removeBinding(viewerMSB); } viewerMSB = null; } } // helping classes // //////////////// /** * Enforces selection in the control: * <ul> * <li>disallows selection changes when the ridget is "output only"</li> * <li>disallows multiple selection is the selection type of the ridget is {@link ISelectableRidget.SelectionType#SINGLE}</li> * </ul> */ private final class SelectionTypeEnforcer extends SelectionAdapter { @Override public void widgetSelected(final SelectionEvent e) { if (isOutputOnly()) { // undo user selection when "output only" final AbstractTableViewer tableViewer = getTableViewer(); if (tableViewer != null) { tableViewer.setSelection(new StructuredSelection(getSelection())); } } else if (SelectionType.SINGLE.equals(getSelectionType())) { if (getUiSelectionCount() > 1) { Assert.isTrue(e.widget == getUIControl()); // ignore this event e.doit = false; // set selection to most recent item setUiSelection(e.item); // fire event final Event event = new Event(); event.type = SWT.Selection; event.doit = true; getUIControl().notifyListeners(SWT.Selection, event); } } } } /** * Notifies the double-click listeners when either a space or CR has been pressed. */ private final class TableKeyListener extends KeyAdapter { @Override public void keyPressed(final KeyEvent e) { if (e.character == ' ' || e.character == SWT.CR) { // the passed event (null) will not be read by the ClickForwarder getClickForwarder().mouseDoubleClick(null); } } } }