/******************************************************************************* * Copyright (c) 2012, 2017 Original authors 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: * Original authors and others - initial API and implementation ******************************************************************************/ package org.eclipse.nebula.widgets.nattable.selection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.nebula.widgets.nattable.data.IRowDataProvider; import org.eclipse.nebula.widgets.nattable.layer.ILayerListener; import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent; import org.eclipse.nebula.widgets.nattable.selection.command.SelectRowsCommand; import org.eclipse.nebula.widgets.nattable.selection.event.ColumnSelectionEvent; import org.eclipse.nebula.widgets.nattable.selection.event.ISelectionEvent; import org.eclipse.nebula.widgets.nattable.util.ObjectUtils; /** * Implementation of ISelectionProvider to add support for JFace selection * handling. * * @param <T> * The type of objects provided by the IRowDataProvider */ public class RowSelectionProvider<T> implements ISelectionProvider, ILayerListener { /** * The SelectionLayer this ISelectionProvider is connected to. */ private SelectionLayer selectionLayer; /** * The IRowDataProvider to access the selected row data. */ private IRowDataProvider<T> rowDataProvider; /** * Flag to determine if only fully selected rows should be used to populate * the selection or if any selection should be populated. Default is to only * populate fully selected rows. */ private final boolean fullySelectedRowsOnly; /** * Flag to configure whether only SelectionChangedEvents should be fired if * the row selection changes or even if you just select another column. */ private final boolean handleSameRowSelection; /** * Collection of ISelectionChangedListeners to this ISelectionProvider */ private Set<ISelectionChangedListener> listeners = new HashSet<ISelectionChangedListener>(); /** * Locally stored previous selection which is used to determine if a * SelectionChangedEvent should be fired. It is used to avoid firing events * if the same row is selected again (default). If handleSameRowSelection is * set to <code>true</code>, this value is not evaluated at runtime. */ private ISelection previousSelection; /** * Flag to configure whether <code>setSelection()</code> should add or set * the selection. * <p> * This was added for convenience because the initial code always added the * selection on <code>setSelection()</code> by creating a SelectRowsCommand * with the withControlMask set to <code>true</code>. Looking at the * specification, <code>setSelection()</code> is used to set the <b>new</b> * selection. So the default here is now to set instead of add. But for * convenience to older code that relied on the add behaviour it is now * possible to change it back to adding. * </p> */ private boolean addSelectionOnSet = false; /** * Flag to configure if row selection should be provided in case a column * selection happened. * <p> * To understand this flag consider a SelectionLayer that is configured for * full row selection by using the RowSelectionModel. On performing column * selection, all rows would get selected. This matches the specification * because of the RowSelectionModel and this RowSelectionProvider would * process every selected row. * </p> * <p> * As described in Bug 421848 this causes serious issues for huge datasets. * To avoid such issues by still providing the ability to perform and * provide multiple row selections, this flag was introduced. Setting the * value to <code>false</code> will avoid processing in case of column * selections. * </p> * * @see RowSelectionModel */ private boolean processColumnSelection = true; /** * Create a RowSelectionProvider that only handles fully selected rows and * only fires SelectionChangedEvents if the row selection changes. * * @param selectionLayer * The SelectionLayer this ISelectionProvider should be connected * to. * @param rowDataProvider * The IRowDataProvider that should be used to access the * selected row data. */ public RowSelectionProvider( SelectionLayer selectionLayer, IRowDataProvider<T> rowDataProvider) { this(selectionLayer, rowDataProvider, true); } /** * Create a RowSelectionProvider that only fires SelectionChangedEvents if * the row selection changes. * * @param selectionLayer * The SelectionLayer this ISelectionProvider should be connected * to. * @param rowDataProvider * The IRowDataProvider that should be used to access the * selected row data. * @param fullySelectedRowsOnly * Flag to determine if only fully selected rows should be used * to populate the selection or if any selection should be * populated. */ public RowSelectionProvider( SelectionLayer selectionLayer, IRowDataProvider<T> rowDataProvider, boolean fullySelectedRowsOnly) { this(selectionLayer, rowDataProvider, fullySelectedRowsOnly, false); } /** * Create a RowSelectionProvider configured with the given parameters. * * @param selectionLayer * The SelectionLayer this ISelectionProvider should be connected * to. * @param rowDataProvider * The IRowDataProvider that should be used to access the * selected row data. * @param fullySelectedRowsOnly * Flag to determine if only fully selected rows should be used * to populate the selection or if any selection should be * populated. * @param handleSameRowSelection * Flag to configure whether only SelectionChangedEvents should * be fired if the row selection changes or even if you just * select another column. */ public RowSelectionProvider( SelectionLayer selectionLayer, IRowDataProvider<T> rowDataProvider, boolean fullySelectedRowsOnly, boolean handleSameRowSelection) { this.selectionLayer = selectionLayer; this.rowDataProvider = rowDataProvider; this.fullySelectedRowsOnly = fullySelectedRowsOnly; this.handleSameRowSelection = handleSameRowSelection; this.selectionLayer.addLayerListener(this); } /** * Updates this RowSelectionProvider so it handles the selection of another * SelectionLayer and IRowDataProvider. * <p> * This method was introduced to add support for multiple selection provider * within one part. As replacing the selection provider during the lifetime * of a part is not properly supported by the workbench, this implementation * adds the possibility to exchange the control that serves as selection * provider by exchanging the references in the selection provider itself. * </p> * * @param selectionLayer * The SelectionLayer this ISelectionProvider should be connected * to. * @param rowDataProvider * The IRowDataProvider that should be used to access the * selected row data. */ public void updateSelectionProvider(SelectionLayer selectionLayer, IRowDataProvider<T> rowDataProvider) { // unregister as listener from the current set SelectionLayer this.selectionLayer.removeLayerListener(this); // update the references on which this RowSelectionProvider should // operate this.selectionLayer = selectionLayer; this.rowDataProvider = rowDataProvider; // register on the new set SelectionLayer as listener this.selectionLayer.addLayerListener(this); } @Override public void addSelectionChangedListener(ISelectionChangedListener listener) { if (listener != null) { this.listeners.add(listener); } } @Override public ISelection getSelection() { return populateRowSelection(this.selectionLayer, this.rowDataProvider, this.fullySelectedRowsOnly); } @Override public void removeSelectionChangedListener(ISelectionChangedListener listener) { this.listeners.remove(listener); } @SuppressWarnings("unchecked") @Override public void setSelection(ISelection selection) { if (this.selectionLayer != null && selection instanceof IStructuredSelection) { if (!this.addSelectionOnSet || selection.isEmpty()) { this.selectionLayer.clear(false); } if (!selection.isEmpty()) { List<T> rowObjects = ((IStructuredSelection) selection).toList(); Set<Integer> rowPositions = new HashSet<Integer>(); for (T rowObject : rowObjects) { int rowIndex = this.rowDataProvider.indexOfRowObject(rowObject); int rowPosition = this.selectionLayer.getRowPositionByIndex(rowIndex); rowPositions.add(Integer.valueOf(rowPosition)); } int intValue = -1; if (!rowPositions.isEmpty()) { Integer max = Collections.max(rowPositions); intValue = max.intValue(); } if (intValue >= 0) { this.selectionLayer.doCommand( new SelectRowsCommand( this.selectionLayer, 0, ObjectUtils.asIntArray(rowPositions), false, true, intValue)); } } else { this.selectionLayer.fireCellSelectionEvent( this.selectionLayer.getLastSelectedCell().columnPosition, this.selectionLayer.getLastSelectedCell().rowPosition, false, false, false); } } } @Override public void handleLayerEvent(ILayerEvent event) { if (event instanceof ISelectionEvent) { ISelection selection = getSelection(); if ((this.handleSameRowSelection || !selection.equals(this.previousSelection)) && !(!this.processColumnSelection && event instanceof ColumnSelectionEvent)) { try { for (ISelectionChangedListener listener : this.listeners) { listener.selectionChanged(new SelectionChangedEvent(this, selection)); } } finally { this.previousSelection = selection; } } } } static <T> StructuredSelection populateRowSelection( SelectionLayer selectionLayer, IRowDataProvider<T> rowDataProvider, boolean fullySelectedRowsOnly) { List<T> rowObjects = SelectionUtils.getSelectedRowObjects(selectionLayer, rowDataProvider, fullySelectedRowsOnly); return rowObjects.isEmpty() ? StructuredSelection.EMPTY : new StructuredSelection(rowObjects); } /** * Configure whether <code>setSelection()</code> should add or set the * selection. * <p> * This was added for convenience because the initial code always added the * selection on <code>setSelection()</code> by creating a SelectRowsCommand * with the withControlMask set to <code>true</code>. Looking at the * specification, <code>setSelection()</code> is used to set the <b>new</b> * selection. So the default here is now to set instead of add. But for * convenience to older code that relied on the add behaviour it is now * possible to change it back to adding. * </p> * * @param addSelectionOnSet * <code>true</code> to add the selection on calling * <code>setSelection()</code> The default is <code>false</code> * to behave like specified in RowSelectionProvider */ public void setAddSelectionOnSet(boolean addSelectionOnSet) { this.addSelectionOnSet = addSelectionOnSet; } /** * Configure whether column selections should start row selection processing * or not. * <p> * This is necessary to handle issues with huge datasets. Dependent on * different configurations, selecting a column can cause the selection of * all rows in a table, which would then lead to populate the whole dataset * as selection via this provider. Setting the * {@link RowSelectionProvider#processColumnSelection} flag to * <code>false</code> will skip processing to avoid such issues. * </p> * * @param processColumnSelection * <code>true</code> to process row selection in case of column * selections (default) <code>false</code> to skip processing in * case of column selections to avoid issues on large datasets. */ public void setProcessColumnSelection(boolean processColumnSelection) { this.processColumnSelection = processColumnSelection; } }