/******************************************************************************* * 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.optional; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Arrays; 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.beans.BeansObservables; import org.eclipse.core.databinding.beans.PojoObservables; import org.eclipse.core.databinding.observable.list.IObservableList; 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.equinox.log.Logger; import org.eclipse.nebula.widgets.compositetable.AbstractNativeHeader; import org.eclipse.nebula.widgets.compositetable.CompositeTable; import org.eclipse.nebula.widgets.compositetable.IRowContentProvider; import org.eclipse.nebula.widgets.compositetable.IRowFocusListener; import org.eclipse.nebula.widgets.compositetable.RowConstructionListener; import org.eclipse.nebula.widgets.compositetable.ScrollEvent; import org.eclipse.nebula.widgets.compositetable.ScrollListener; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.riena.core.Log4r; import org.eclipse.riena.core.util.Nop; import org.eclipse.riena.core.util.ReflectionUtils; import org.eclipse.riena.core.wire.Wire; import org.eclipse.riena.internal.ui.ridgets.swt.StructuredViewerFilterHolder; import org.eclipse.riena.ui.common.IComplexComponent; import org.eclipse.riena.ui.common.ISortableByColumn; import org.eclipse.riena.ui.ridgets.IExtendedRidgetProperties; import org.eclipse.riena.ui.ridgets.IMarkableRidget; import org.eclipse.riena.ui.ridgets.IRidget; import org.eclipse.riena.ui.ridgets.IRowRidget; import org.eclipse.riena.ui.ridgets.ISelectableRidget; import org.eclipse.riena.ui.ridgets.ITextRidget; import org.eclipse.riena.ui.ridgets.IToggleButtonRidget; import org.eclipse.riena.ui.ridgets.listener.IClickListener; import org.eclipse.riena.ui.ridgets.listener.ISelectionListener; import org.eclipse.riena.ui.ridgets.swt.AbstractSWTWidgetRidget; import org.eclipse.riena.ui.ridgets.swt.AbstractSelectableIndexedRidget; import org.eclipse.riena.ui.ridgets.swt.MarkerSupport; import org.eclipse.riena.ui.ridgets.swt.SortableComparator; import org.eclipse.riena.ui.ridgets.swt.optional.ICompositeTableRidget; import org.eclipse.riena.ui.ridgets.swt.uibinding.SwtControlRidgetMapper; import org.eclipse.riena.ui.ridgets.uibinding.IBindingPropertyLocator; import org.eclipse.riena.ui.ridgets.uibinding.IControlRidgetMapper; import org.eclipse.riena.ui.swt.lnf.LnFUpdater; import org.eclipse.riena.ui.swt.utils.SWTBindingPropertyLocator; /** * A ridget for nebula's {@link CompositeTable} - this is a table with an instance of an arbitrary composite in each row. */ public class CompositeTableRidget extends AbstractSelectableIndexedRidget implements ICompositeTableRidget { private final static Logger LOGGER = Log4r.getLogger(Activator.getDefault(), CompositeTableRidget.class); private final CTRowToRidgetMapper rowToRidgetMapper; private final SelectionSynchronizer selectionSynchronizer; private final Map<Integer, Boolean> sortableColumnsMap; private final Map<Integer, Comparator<Object>> comparatorMap; private final SelectionListener sortListener; private final ScrollListener compositeTableScrollListener; /** * An observable list supplied via bindToModel(...). May be null. May change at any time. */ private IObservableList modelObservables; /** * A copy of rowObservables as an array. This is used to feed the rows / rowRidgets in the table. Will be updated when #updateFromModel() is called. The * array will be re-ordered if sorting is applied. Never null. */ private Object[] rowValues; /** * A copy of the <b>unsorted</b> rowValues. It provides the ability to restore the unsorted state of rowValues. This array is null if rowValues is unsorted. */ private Object[] rowValuesUnsorted; private Class<? extends Object> rowBeanClass; private Class<? extends Object> rowRidgetClass; private String[] columnHeaders; private boolean isSortedAscending; private int sortedColumn; private boolean flushCache = false; private boolean relayoutAllParents = false; private final LnFUpdater lnfUpdater = LnFUpdater.getInstance(); public CompositeTableRidget() { Assert.isLegal(!SelectionType.MULTI.equals(getSelectionType())); rowToRidgetMapper = new CTRowToRidgetMapper(); selectionSynchronizer = new SelectionSynchronizer(); compositeTableScrollListener = new CompositeTableScrollListener(); sortableColumnsMap = new HashMap<Integer, Boolean>(); comparatorMap = new HashMap<Integer, Comparator<Object>>(); sortListener = new ColumnSortListener(); isSortedAscending = true; rowValues = new Object[0]; sortedColumn = -1; getSingleSelectionObservable().addValueChangeListener(new IValueChangeListener() { public void handleValueChange(final ValueChangeEvent event) { final Object value = event.getObservableValue().getValue(); getMultiSelectionObservable().clear(); if (value != null) { getMultiSelectionObservable().add(value); } } }); addPropertyChangeListener(IRidget.PROPERTY_ENABLED, new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { refreshRowStyles(); } }); addPropertyChangeListener(IMarkableRidget.PROPERTY_OUTPUT_ONLY, new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { refreshRowStyles(); } }); } @Override protected void checkUIControl(final Object uiControl) { checkType(uiControl, CompositeTable.class); } /** * This should fix an issue with cutoff rows under some circumstances. If you do not encounter any problems with cut off rows you don't need to take any * further actions. Let the user configure the re layout behavior on scrollevents. If not set the default is that we don't flush the caches and don't layout * all parents on scrollevents . * <p> * Attention: re layout big hierarchy trees of partens can cause performance issues * * @param flushCashe * configures if we flush the caches on every scrollevent. * @param relayoutAllParents * configures if we re layout only the parent or all parents of the table. */ public void configureLayoutBehaviorForScrollEvent(final boolean flushCache, final boolean relayoutAllParents) { this.flushCache = flushCache; this.relayoutAllParents = relayoutAllParents; } /** * Returns the flushCache and the relayoutAllPartens field. * * @return an array with two values */ public boolean[] getLayoutBehaviorOnScrollEvent() { final boolean[] result = new boolean[2]; result[0] = flushCache; result[1] = relayoutAllParents; return result; } @Override protected void bindUIControl() { final CompositeTable control = getUIControl(); if (control != null) { control.addRowConstructionListener(rowToRidgetMapper); control.addRowContentProvider(rowToRidgetMapper); updateControl(control); control.addRowFocusListener(selectionSynchronizer); control.addScrollListener(compositeTableScrollListener); getSingleSelectionObservable().addValueChangeListener(selectionSynchronizer); if (getHeader() != null) { for (final TableColumn column : getHeader().getColumns()) { column.addSelectionListener(sortListener); } } refreshRowStyles(); } } @Override protected void unbindUIControl() { super.unbindUIControl(); final CompositeTable control = getUIControl(); if (control != null) { if (getHeader() != null) { for (final TableColumn column : getHeader().getColumns()) { column.removeSelectionListener(sortListener); } } getSingleSelectionObservable().removeValueChangeListener(selectionSynchronizer); control.removeRowFocusListener(selectionSynchronizer); control.removeRowContentProvider(rowToRidgetMapper); control.removeRowConstructionListener(rowToRidgetMapper); control.removeScrollListener(compositeTableScrollListener); } } @Override protected List<?> getRowObservables() { return modelObservables != null ? Arrays.asList(rowValues) : null; } @Override public Object getOption(final int index) { if (getRowObservables() == null || index < 0 || index >= getOptionCount()) { throw new IllegalArgumentException("index: " + index); //$NON-NLS-1$ } return rowValues[index]; } public void bindToModel(final IObservableList rowObservables, final Class<? extends Object> rowClass, final Class<? extends Object> rowRidgetClass) { Assert.isLegal(IRowRidget.class.isAssignableFrom(rowRidgetClass)); bindToModel(rowObservables, rowClass, rowRidgetClass, null); } public void bindToModel(final IObservableList rowObservables, final Class<? extends Object> rowClass, final Class<? extends Object> rowRidgetClass, final String[] columnHeaders) { Assert.isLegal(IRowRidget.class.isAssignableFrom(rowRidgetClass)); unbindUIControl(); modelObservables = rowObservables; rowValues = new Object[0]; rowValuesUnsorted = null; this.rowBeanClass = rowClass; this.rowRidgetClass = rowRidgetClass; 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 Class<? extends Object> rowRidgetClass, final String[] columnHeaders) { IObservableList rowBeansObservables; if (AbstractSWTWidgetRidget.isBean(rowClass)) { rowBeansObservables = BeansObservables.observeList(listHolder, listPropertyName); } else { rowBeansObservables = PojoObservables.observeList(listHolder, listPropertyName); } bindToModel(rowBeansObservables, rowClass, rowRidgetClass, columnHeaders); } public void bindToModel(final Object listHolder, final String listPropertyName, final Class<? extends Object> rowClass, final Class<? extends Object> rowRidgetClass) { bindToModel(listHolder, listPropertyName, rowClass, rowRidgetClass, null); } @Override public void updateFromModel() { super.updateFromModel(); final CompositeTable control = getUIControl(); if (modelObservables != null) { rowValues = modelObservables.toArray(); } else { rowValues = new Object[0]; } rowValuesUnsorted = null; if (control != null) { updateControl(control); } else { refreshSelection(); } } @Override public int getSelectionIndex() { final Object selection = getSingleSelectionObservable().getValue(); return indexOfOption(selection); } @Override public int[] getSelectionIndices() { final int index = getSelectionIndex(); return index == -1 ? new int[0] : new int[] { index }; } @Override public CompositeTable getUIControl() { return (CompositeTable) super.getUIControl(); } @Override public int indexOfOption(final Object option) { int result = -1; if (option != null) { result = rowIndexOfOption(option); } return result; } @Override public boolean isDisableMandatoryMarker() { return true; } @Override public void setSelectionType(final SelectionType selectionType) { if (SelectionType.MULTI.equals(selectionType)) { throw new IllegalArgumentException("SelectionType.MULTI is not supported by the UI-control"); //$NON-NLS-1$ } super.setSelectionType(selectionType); } @Override public void setSelection(final List<?> newSelection) { readAndDispatch(); super.setSelection(newSelection); readAndDispatch(); } public void setComparator(final int columnIndex, final Comparator<Object> comparator) { checkColumnRange(columnIndex); final Integer key = Integer.valueOf(columnIndex); if (comparator != null) { comparatorMap.put(key, comparator); } else { comparatorMap.remove(key); } if (columnIndex == sortedColumn) { applyComparator(); } } @Override protected StructuredViewerFilterHolder getFilterHolder() { return null; // filtering is not supported } // methods of ISortableByColumn /////////////////////////////// /** * {@inheritDoc} * <p> * Implementation note: there is no API to query the sorted column on CompositeTable. The returned value is computed with information held in the ridget. */ public int getSortedColumn() { int result = -1; final Integer key = Integer.valueOf(sortedColumn); if (getUIControl() != null && comparatorMap.containsKey(key)) { result = sortedColumn; } return result; } /** * {@inheritDoc} * <p> * Implementation notes: to have columns that are sortable by the user, the CompositeTable bound to this ridget must have an {@link AbstractNativeHeader}. * In any other case this method will return false. */ public boolean isColumnSortable(final int columnIndex) { checkColumnRange(columnIndex); boolean result = false; if (getHeader() != null) { final Integer key = Integer.valueOf(columnIndex); final Boolean sortable = sortableColumnsMap.get(columnIndex); if (sortable == null || Boolean.TRUE.equals(sortable)) { result = comparatorMap.get(key) != null; } } return result; } /** * {@inheritDoc} * <p> * Implementation note: there is no API to query the sorted column on CompositeTable. The returned value is computed with information held in the ridget. */ public boolean isSortedAscending() { boolean result = false; if (getSortedColumn() != -1) { result = isSortedAscending; } return result; } 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 setSortedAscending(final boolean ascending) { if (isSortedAscending != ascending) { final boolean oldSortedAscending = isSortedAscending; isSortedAscending = ascending; applyComparator(); 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(); firePropertyChange(ISortableByColumn.PROPERTY_SORTED_COLUMN, oldSortedColumn, sortedColumn); } } /** * {@link CompositeTableRidget} does currently not support this operation. * * @throws UnsupportedOperationException * when invoked */ @Override public void addSelectionListener(final ISelectionListener selectionListener) { throw new UnsupportedOperationException("not supported"); //$NON-NLS-1$ } /** * {@link CompositeTableRidget} does currently not support this operation. * * @throws UnsupportedOperationException * when invoked */ @Override public void removeSelectionListener(final ISelectionListener selectionListener) { throw new UnsupportedOperationException("not supported"); //$NON-NLS-1$ } /** * {@link CompositeTableRidget} does currently not support this operation, because the CompositeTable dosn't fire mouse events. * * @throws UnsupportedOperationException * when invoked */ @Override public void addClickListener(final IClickListener listener) { throw new UnsupportedOperationException("not supported"); //$NON-NLS-1$ } /** * {@link CompositeTableRidget} does currently not support this operation, because the CompositeTable dosn't fire mouse events. * * @throws UnsupportedOperationException * when invoked */ @Override public void removeClickListener(final IClickListener listener) { throw new UnsupportedOperationException("not supported"); //$NON-NLS-1$ } // helping methods ////////////////// private void applyComparator() { final CompositeTable control = getUIControl(); if (control != null) { Comparator<Object> comparator = null; if (sortedColumn != -1) { final Integer key = Integer.valueOf(sortedColumn); comparator = comparatorMap.get(key); } if (comparator != null) { setSortColumnOnHeader(sortedColumn); final int direction = isSortedAscending ? SWT.UP : SWT.DOWN; setSortDirectionOnHeader(direction); if (rowValuesUnsorted == null) { rowValuesUnsorted = new Object[rowValues.length]; System.arraycopy(rowValues, 0, rowValuesUnsorted, 0, rowValuesUnsorted.length); } final SortableComparator sortableComparator = new SortableComparator(this, comparator); Arrays.sort(rowValues, sortableComparator); control.refreshAllRows(); } else { setSortColumnOnHeader(sortedColumn); setSortDirectionOnHeader(SWT.NONE); if (rowValuesUnsorted != null) { Assert.isLegal(rowValuesUnsorted.length == rowValues.length); System.arraycopy(rowValuesUnsorted, 0, rowValues, 0, rowValues.length); rowValuesUnsorted = null; control.refreshAllRows(); } } } } private void checkColumnRange(final int columnIndex) { Assert.isLegal(-1 < columnIndex, "columnIndex out of range: " + columnIndex); //$NON-NLS-1$ } private AbstractNativeHeader getHeader() { final CompositeTable control = getUIControl(); final Control header = control.getHeader(); return (AbstractNativeHeader) (header instanceof AbstractNativeHeader ? header : null); } /** * CompositeTable.setSelection(x,y) is asynchronous. This means that: - we have to fully process the event-queue before, to avoid pending events adding * selection changes after our call, via asyncexec(op)). - we have to fully process the event-queue afterwards, to make sure the selection is applied to the * widget. * <p> * Typically this method will be called before AND after ct.setSelection(x,y); */ private void readAndDispatch() { final CompositeTable control = getUIControl(); if (control != null) { final Display display = control.getDisplay(); while (display.readAndDispatch()) { Nop.reason("keep working"); //$NON-NLS-1$ } } } private void refreshRowStyle(final Control rowControl, final boolean isEnabled, final boolean isOutput, final Color bgColor) { rowControl.setBackground(bgColor); if (MarkerSupport.isHideDisabledRidgetContent()) { rowControl.setVisible(isEnabled); } rowControl.setEnabled(isEnabled && !isOutput); } private void refreshRowStyles() { final CompositeTable table = getUIControl(); final boolean enabled = isEnabled(); final boolean output = isOutputOnly(); if (table != null) { // update each row for (final Control rowControl : table.getRowControls()) { final Color bgColor = table.getBackground(); refreshRowStyle(rowControl, enabled, output, bgColor); } // Updates the color of contentPane (=composite that holds the rows). // // This is a clever trick: MarkerSupport manipulates // the bgColor of the CompositeTable, we manipulate the bgColor // of the internal contentPane - this way we don't get in each // other's way. By using the color for table when enabled we make // sure that we apply the 'right' color from MarkerSupport. final Control[] children = table.getChildren(); final Control contentPane = children[children.length - 1]; final Display display = table.getDisplay(); final Color cpBgColor = enabled ? table.getBackground() : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); contentPane.setBackground(cpBgColor); } } private int rowIndexOfOption(final Object element) { int result = -1; for (int i = 0; result == -1 && i < rowValues.length; i++) { if (rowValues[i] == element) { result = i; } } return result; } private void setSortDirectionOnHeader(final int direction) { final AbstractNativeHeader header = getHeader(); if (header != null) { header.setSortDirection(direction); } } private void setSortColumnOnHeader(final int columnIndex) { final AbstractNativeHeader header = getHeader(); if (header != null) { header.setSortColumn(columnIndex); } } /** * Sets the text of every column header. */ private void updateHeader() { if (columnHeaders == null) { return; } final AbstractNativeHeader header = getHeader(); if (header != null) { final TableColumn[] columns = header.getColumns(); for (int i = 0; i < columns.length; i++) { if (i < columnHeaders.length) { columns[i].setText(columnHeaders[i]); } } } } private void updateControl(final CompositeTable control) { control.setRedraw(false); try { control.setNumRowsInCollection(rowValues.length); updateHeader(); applyComparator(); updateSelection(); } finally { control.setRedraw(true); } } // helping classes ////////////////// /** * Re-applies ridget selection to control (if selection exists), otherwise clears ridget selection */ private void updateSelection() { final CompositeTable control = getUIControl(); if (control != null) { final Object selection = getSingleSelectionObservable().getValue(); final int index = rowIndexOfOption(selection); if (index > -1) { final int row = index - control.getTopRow(); readAndDispatch(); control.setSelection(0, row); readAndDispatch(); } else { clearSelection(); control.clearSelection(); } } } /** * Binds and configures Ridgets to a Row control. */ private final class CTRowToRidgetMapper extends RowConstructionListener implements IRowContentProvider { @Override public void headerConstructed(final Control newHeader) { // unused } @Override public void rowConstructed(final Control newRow) { final IComplexComponent rowControl = (IComplexComponent) newRow; if (rowRidgetClass == null) { return; } final IRowRidget rowRidget = (IRowRidget) ReflectionUtils.newInstance(rowRidgetClass, (Object[]) null); final IBindingPropertyLocator locator = SWTBindingPropertyLocator.getInstance(); for (final Object control : rowControl.getUIControls()) { final String bindingProperty = locator.locateBindingProperty(control); if (bindingProperty != null) { final IRidget ridget = createRidget(control); ridget.setUIControl(control); ridget.setController(rowRidget); rowRidget.addRidget(bindingProperty, ridget); new RowChangeNotifier().install(ridget); } else { final String message = String.format("widget without binding property: %s : %s", rowControl.getClass(), //$NON-NLS-1$ control); LOGGER.log(LogService.LOG_WARNING, message); } } if (Activator.getDefault() != null) { Wire.instance(rowRidget).andStart(Activator.getDefault().getContext()); } newRow.setData("rowRidget", rowRidget); //$NON-NLS-1$ } public void refresh(final CompositeTable table, final int index, final Control row) { if (index < rowValues.length && index > -1) { final Object rowBean = rowValues[index]; Assert.isLegal(rowBeanClass.isAssignableFrom(rowBean.getClass())); final IRowRidget rowRidget = (IRowRidget) row.getData("rowRidget"); //$NON-NLS-1$ rowRidget.setData(rowBean); rowRidget.configureRidgets(); rowRidget.setConfigured(true); refreshRowStyle(row, isEnabled(), isOutputOnly(), table.getBackground()); if (row instanceof Composite) { lnfUpdater.updateUIControls((Composite) row, false); } } } private IRidget createRidget(final Object control) { final IControlRidgetMapper<Object> mapper = SwtControlRidgetMapper.getInstance(); final Class<? extends IRidget> ridgetClass = mapper.getRidgetClass(control); return ReflectionUtils.newInstance(ridgetClass); } } /** * Observes the ridgets of the table�s rows */ private class RowChangeNotifier implements PropertyChangeListener { public void install(final IRidget ridget) { ridget.addPropertyChangeListener(ITextRidget.PROPERTY_TEXT, this); ridget.addPropertyChangeListener(ISelectableRidget.PROPERTY_SELECTION, this); ridget.addPropertyChangeListener(IToggleButtonRidget.PROPERTY_SELECTED, this); } public void propertyChange(final PropertyChangeEvent evt) { // delegation propertyChangeSupport.firePropertyChange(IExtendedRidgetProperties.PROPERTY_ROW_VALUE, evt.getOldValue(), evt.getNewValue()); } } /** * Layouts the parent Composite of the compositeTable whenever we scroll the table. This should fix an drawing issue, where some rows in the Table get cut * off while scrolling. */ private class CompositeTableScrollListener extends ScrollListener { @Override public void tableScrolled(final ScrollEvent scrollEvent) { CompositeTableRidget.this.getUIControl().getParent().layout(flushCache, relayoutAllParents); } } /** * Updates the selection in a CompositeTable control, when the value of the (single selection) observable changes and vice versa. */ private final class SelectionSynchronizer implements IRowFocusListener, IValueChangeListener { private boolean isArriving = false; private boolean isSelecting = false; public void arrive(final CompositeTable sender, final int currentObjectOffset, final Control newRow) { if (getRowObservables() == null) { return; } if (isSelecting) { return; } isArriving = true; try { final int selectionIndex = rowIndexOfOption(getSingleSelectionObservable().getValue()); if (currentObjectOffset != selectionIndex) { setSelection(currentObjectOffset); } } finally { isArriving = false; } } public void depart(final CompositeTable sender, final int currentObjectOffset, final Control row) { // unused } public boolean requestRowChange(final CompositeTable sender, final int currentObjectOffset, final Control row) { return true; } public void handleValueChange(final ValueChangeEvent event) { if (isArriving) { return; } isSelecting = true; try { updateSelection(); } finally { isSelecting = false; } } } /** * Selection listener for table headers that changes the sort order of a column according to the information stored in the ridget. */ private final class ColumnSortListener extends SelectionAdapter { @Override public void widgetSelected(final SelectionEvent e) { final TableColumn column = (TableColumn) e.widget; final int columnIndex = column.getParent().indexOf(column); final int direction = column.getParent().getSortDirection(); if (columnIndex == sortedColumn) { if (direction == SWT.UP) { setSortedAscending(false); } else if (direction == SWT.DOWN) { setSortedColumn(-1); } } else if (isColumnSortable(columnIndex)) { setSortedColumn(columnIndex); if (direction == SWT.NONE) { setSortedAscending(true); } } column.getParent().showSelection(); } } }