Skip to content

Content of file GridTableViewerComposite.java

/*******************************************************************************
 * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 * Alexandra Buzila - initial API and implementation
 * Johannes Faltermeier - initial API and implementation
 * Christian W. Damus - bugs 534829, 530314, 547271
 ******************************************************************************/
package org.eclipse.emf.ecp.view.spi.table.nebula.grid;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

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.databinding.observable.value.WritableValue;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.emf.databinding.EMFDataBindingContext;
import org.eclipse.emf.ecp.edit.spi.swt.table.ECPFilterableCell;
import org.eclipse.emf.ecp.view.spi.table.nebula.grid.GridControlSWTRenderer.CustomGridTableViewer;
import org.eclipse.emf.ecp.view.spi.table.nebula.grid.menu.GridColumnAction;
import org.eclipse.emf.ecp.view.spi.table.nebula.grid.messages.Messages;
import org.eclipse.emfforms.common.Feature;
import org.eclipse.emfforms.common.Property;
import org.eclipse.emfforms.spi.swt.table.AbstractTableViewerComposite;
import org.eclipse.emfforms.spi.swt.table.ColumnConfiguration;
import org.eclipse.emfforms.spi.swt.table.TableConfiguration;
import org.eclipse.emfforms.spi.swt.table.TableControl;
import org.eclipse.emfforms.spi.swt.table.TableViewerComparator;
import org.eclipse.emfforms.spi.swt.table.TableViewerSWTCustomization;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuListener2;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.layout.AbstractColumnLayout;
import org.eclipse.jface.viewers.CellLabelProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.viewers.ViewerColumn;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.nebula.jface.gridviewer.GridColumnLayout;
import org.eclipse.nebula.jface.gridviewer.GridTableViewer;
import org.eclipse.nebula.jface.gridviewer.GridViewerColumn;
import org.eclipse.nebula.jface.gridviewer.GridViewerRow;
import org.eclipse.nebula.widgets.grid.Grid;
import org.eclipse.nebula.widgets.grid.GridColumn;
import org.eclipse.nebula.widgets.grid.GridItem;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Widget;

/**
 * A {@link Composite} containing a {@link GridTableViewer}.
 *
 * @author Jonas Helming
 *
 */
public class GridTableViewerComposite extends AbstractTableViewerComposite<GridTableViewer> {

	private static final Map<Feature, Function<GridTableViewerComposite, ? extends IMenuListener>> FEATURE_MENU_LISTENERS = new HashMap<>();

	private static final Map<Feature, Function<GridTableViewerComposite, ? extends ViewerFilter>> FEATURE_VIEWER_FILTERS = new HashMap<>();

	private GridTableViewer gridTableViewer;
	private Point lastKnownPointer;

	private final IObservableValue<Feature> activeFilteringMode = new WritableValue<>();

	private TableViewerComparator comparator;

	private List<Integer> sortableColumns;

	static {
		FEATURE_MENU_LISTENERS.put(TableConfiguration.FEATURE_COLUMN_HIDE_SHOW,
			comp -> comp.new ColumnHideShowMenuListener());
		FEATURE_MENU_LISTENERS.put(TableConfiguration.FEATURE_COLUMN_FILTER,
			comp -> comp.new ColumnFilterMenuListener(ColumnConfiguration.FEATURE_COLUMN_FILTER,
				Messages.GridTableViewerComposite_toggleFilterControlsAction));
		FEATURE_MENU_LISTENERS.put(TableConfiguration.FEATURE_COLUMN_REGEX_FILTER,
			comp -> comp.new ColumnFilterMenuListener(ColumnConfiguration.FEATURE_COLUMN_REGEX_FILTER,
				Messages.GridTableViewerComposite_toggleRegexFilterControlsAction));

		FEATURE_VIEWER_FILTERS.put(TableConfiguration.FEATURE_COLUMN_FILTER,
			comp -> comp.new GridColumnFilterViewerFilter());
		FEATURE_VIEWER_FILTERS.put(TableConfiguration.FEATURE_COLUMN_REGEX_FILTER,
			comp -> comp.new GridColumnRegexFilterViewerFilter());
	}

	/**
	 * Default constructor.
	 *
	 * @param parent the parent {@link Composite}
	 * @param style the style bits
	 * @param inputObject the input object
	 * @param customization the {@link TableViewerSWTCustomization}
	 * @param title the title
	 * @param tooltip the tooltip
	 */
	public GridTableViewerComposite(Composite parent, int style, Object inputObject,
		TableViewerSWTCustomization customization,
IObservableValue title, IObservableValue tooltip) { super(parent, style, inputObject, customization, title, tooltip); activeFilteringMode.addValueChangeListener(this::handleFilteringMode); } @Override public void dispose() { activeFilteringMode.dispose(); super.dispose(); } @Override public GridTableViewer getTableViewer() { return gridTableViewer; } @Override protected GridTableViewer createTableViewer(TableViewerSWTCustomization<GridTableViewer> customization, Composite viewerComposite) { gridTableViewer = customization.createTableViewer(viewerComposite); gridTableViewer.getControl().addMouseMoveListener(event -> lastKnownPointer = new Point(event.x, event.y)); gridTableViewer.getControl().addMouseTrackListener(new MouseTrackAdapter() { @Override public void mouseExit(MouseEvent event) { lastKnownPointer = null; } }); return gridTableViewer; } @Override protected void configureContextMenu(GridTableViewer tableViewer) { final MenuManager menuMgr = new MenuManager(); menuMgr.setRemoveAllWhenShown(true); mapFeatures(FEATURE_MENU_LISTENERS::get, menuMgr::addMenuListener); final Menu menu = menuMgr.createContextMenu(tableViewer.getControl()); tableViewer.getControl().setMenu(menu); } private <T> void mapFeatures(Function<Feature, Function<? super GridTableViewerComposite, T>> mapper, Consumer<? super T> action) { getEnabledFeatures().stream().map(mapper).filter(Objects::nonNull) .map(f -> f.apply(this)).forEach(action); } @Override protected void configureViewerFilters(GridTableViewer tableViewer) { mapFeatures(FEATURE_VIEWER_FILTERS::get, tableViewer::addFilter); } @Override protected AbstractColumnLayout createLayout(Composite viewerComposite) { final GridColumnLayout layout = new GridColumnLayout(); viewerComposite.setLayout(layout); return layout; } @Override public Widget[] getColumns() { return gridTableViewer.getGrid().getColumns(); } @Override public void addColumnListener(ControlListener columnlistener) { for (int i = 0; i < gridTableViewer.getGrid().getColumns().length; i++) { final GridColumn gridColumn = gridTableViewer.getGrid().getColumns()[i]; gridColumn.addControlListener(columnlistener); } } @Override public TableControl getTableControl() { return new TableControl() { @Override public boolean isDisposed() { return getTableViewer().getGrid().isDisposed(); } @Override public int getItemHeight() { return getTableViewer().getGrid().getItemHeight(); } @Override public boolean getHeaderVisible() { return getTableViewer().getGrid().getHeaderVisible(); } @Override public int getHeaderHeight() { return getTableViewer().getGrid().getHeaderHeight(); } @Override public int getItemCount() { return getTableViewer().getGrid().getItemCount(); } }; } @Override protected ViewerColumn createColumn(final ColumnConfiguration config, EMFDataBindingContext emfDataBindingContext, final GridTableViewer tableViewer) { final GridViewerColumn column = new GridViewerColumnBuilder(config) .withDatabinding(emfDataBindingContext) .build(tableViewer); return column; } @Override public void setComparator(final TableViewerComparator comparator, List<Integer> sortableColumns) { this.comparator = comparator; this.sortableColumns = sortableColumns; for (int i = 0; i < getTableViewer().getGrid().getColumns().length; i++) { if (!sortableColumns.contains(i)) { continue; } final int j = i; final GridColumn tableColumn = getTableViewer().getGrid().getColumns()[i]; final SelectionAdapter selectionAdapter = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { setCompareColumn(j); } }; tableColumn.addSelectionListener(selectionAdapter); } } private GridColumn getCurrentColumn() { GridColumn result = null; final Grid grid = getTableViewer().getGrid(); if (lastKnownPointer == null) { // For testability, especially in RCPTT, we may not have any real // tracking of the mouse pointer. So, hope there's a selection final Point[] selectedCells = grid.getCellSelection(); if (selectedCells != null && selectedCells.length > 0) { result = grid.getColumn(selectedCells[0].x); } } else { result = grid.getColumn(lastKnownPointer); } return result; } private ColumnConfiguration getCurrentColumnConfig() { final GridColumn column = getCurrentColumn(); if (column == null) { return null; } return getColumnConfiguration(column); } /** * Query the currently active filtering mode, if filtering is engaged. * * @return one the {@linkplain ColumnConfiguration#FEATURE_COLUMN_FILTER filtering features} * indicating the filtering mode that is active, or {@code null} if the grid is not filtered * * @since 1.21 * @see #setFilteringMode(Feature) * @see ColumnConfiguration#FEATURE_COLUMN_FILTER * @see ColumnConfiguration#FEATURE_COLUMN_REGEX_FILTER */ public Feature getFilteringMode() { return activeFilteringMode == null ? null : activeFilteringMode.getValue(); } /** * Set the currently active filtering mode. * * @param filteringFeature one the {@linkplain ColumnConfiguration#FEATURE_COLUMN_FILTER filtering features} * indicating the filtering mode that is active, or {@code null} if the grid is not to be filtered * * @throws IllegalStateException if the composite is not yet initialized * @throws IllegalArgumentException if the {@code filteringFeature} is not supported by my * table configuration ({@code null}, excepted, of course) * * @since 1.21 * @see #getFilteringMode() * @see ColumnConfiguration#FEATURE_COLUMN_FILTER * @see ColumnConfiguration#FEATURE_COLUMN_REGEX_FILTER */ public void setFilteringMode(Feature filteringFeature) { if (activeFilteringMode == null) { // This can happen while superclass constructor is running, which // calls into polymorphic methods overridden in this class throw new IllegalStateException(); } if (filteringFeature != null && !getEnabledFeatures().contains(filteringFeature)) { throw new IllegalArgumentException(filteringFeature.toString()); } activeFilteringMode.setValue(filteringFeature); } /** * Respond to a change of filtering mode by showing/hiding filter controls * in the columns as appropriate. * * @param event the change in the filtering mode */ private void handleFilteringMode(ValueChangeEvent<? extends Feature> event) { final Boolean showFilterControl = getFilteringMode() != null; boolean recalculateHeader = false; for (final Widget widget : getColumns()) { final Property<Boolean> showProperty = getColumnConfiguration(widget).showFilterControl(); if (!showFilterControl.equals(showProperty.getValue())) { recalculateHeader = true; showProperty.setValue(showFilterControl); } } if (recalculateHeader) { getTableViewer().getGrid().recalculateHeader(); } } // // Nested types // /** * A grid column action whose enablement depends on a enablement of a * {@linkplain ColumnConfiguration#getEnabledFeatures() configuration feature}. */ private abstract class FeatureBasedColumnAction extends GridColumnAction { private final Feature feature; { setCurrentColumnProvider(GridTableViewerComposite.this::getCurrentColumn); } FeatureBasedColumnAction(GridTableViewerComposite gridTableViewerComposite, String actionLabel, Feature feature) { super(gridTableViewerComposite, actionLabel); this.feature = feature; } FeatureBasedColumnAction(GridTableViewerComposite gridTableViewerComposite, String actionLabel, int style, Feature feature) { super(gridTableViewerComposite, actionLabel, style); this.feature = feature; } @Override public boolean isEnabled() { return super.isEnabled() && getEnabledFeatures().contains(feature); } } /** * Column hide/show menu listener. * * @author Mat Hansen * */ private class ColumnHideShowMenuListener implements IMenuListener { @Override public void menuAboutToShow(IMenuManager manager) { final ColumnConfiguration columnConfiguration = getCurrentColumnConfig(); if (columnConfiguration == null) { return; } manager.add(new FeatureBasedColumnAction(GridTableViewerComposite.this, Messages.GridTableViewerComposite_hideColumnAction, ColumnConfiguration.FEATURE_COLUMN_HIDE_SHOW) { @Override public void run() { columnConfiguration.visible().setValue(Boolean.FALSE); } }); manager.add(new FeatureBasedColumnAction(GridTableViewerComposite.this, Messages.GridTableViewerComposite_showAllColumnsAction, ColumnConfiguration.FEATURE_COLUMN_HIDE_SHOW) { @Override public void run() { for (final Widget widget : getColumns()) { getGridTableViewer().getColumnConfiguration(widget).visible().setValue(Boolean.TRUE); } } @Override public boolean isEnabled() { return super.isEnabled() && hasHiddenColumns(); } boolean hasHiddenColumns() { for (final Widget widget : getColumns()) { if (!getGridTableViewer().getColumnConfiguration(widget).visible().getValue()) { return true; } } return false; } }); } } /** * Common definition of a filtering menu listener. * * @author Mat Hansen * */ private class ColumnFilterMenuListener implements IMenuListener2 { private final IValueChangeListener<Feature> radioListener = this::handleRadio; private final Feature feature; private final String label; private GridColumnAction action; ColumnFilterMenuListener(Feature feature, String label) { super(); this.feature = feature; this.label = label; } @Override public void menuAboutToShow(IMenuManager manager) { final ColumnConfiguration columnConfiguration = getCurrentColumnConfig(); if (columnConfiguration == null) { return; } action = createAction(feature, label, columnConfiguration); action.setChecked(getFilteringMode() == feature); manager.add(action); activeFilteringMode.addValueChangeListener(radioListener); } @Override public void menuAboutToHide(IMenuManager manager) { activeFilteringMode.removeValueChangeListener(radioListener); action = null; } GridColumnAction createAction(Feature feature, String label, ColumnConfiguration columnConfiguration) { return new FeatureBasedColumnAction(GridTableViewerComposite.this, label, IAction.AS_RADIO_BUTTON, feature) { @Override public void run() { if (getFilteringMode() == feature) { // We're toggling filtering off setFilteringMode(null); } else { // We're setting filtering to my mode setFilteringMode(feature); } } }; } void handleRadio(ValueChangeEvent<? extends Feature> event) { action.setChecked(event.getObservableValue().getValue() == feature); } } /** * Common viewer filter implementation for column filters. * * @author Mat Hansen * */ private abstract class AbstractGridColumnFilterViewerFilter extends ViewerFilter { private final Feature feature; private final GridTableViewer tableViewer; private final Grid grid; /** * Initializes me with the filtering feature that I implement. * * @param feature my defining feature */ AbstractGridColumnFilterViewerFilter(Feature feature) { super(); this.feature = feature; tableViewer = getTableViewer(); grid = tableViewer.getGrid(); } @Override public boolean select(Viewer viewer, Object parentElement, Object element) { if (!isApplicable(viewer)) { return true; } GridItem dummyItem = null; GridViewerRow viewerRow = null; try { for (final Widget widget : getColumns()) { final ColumnConfiguration config = getColumnConfiguration(widget); // Do we even have a filter to apply? final Object filter = config.matchFilter().getValue(); if (filter == null || String.valueOf(filter).isEmpty()) { continue; } final GridColumn column = (GridColumn) widget; final int columnIndex = tableViewer.getGrid().indexOf(column); final CellLabelProvider labelProvider = tableViewer.getLabelProvider(columnIndex); // Optimize for the standard case final ECPFilterableCell filterable = Adapters.adapt(labelProvider, ECPFilterableCell.class); if (filterable != null) { // Just get the text and filter it final String text = filterable.getFilterableText(element); if (!matchesColumnFilter(text, filter)) { return false; } } else { // We have a filter, but we need something to filter on if (dummyItem == null) { grid.setRedraw(false); dummyItem = new GridItem(grid, SWT.NONE); dummyItem.setData(element); viewerRow = (GridViewerRow) ((CustomGridTableViewer) tableViewer) .getViewerRowFromItem(dummyItem); } // Update the cell, the slow way final ViewerCell cell = viewerRow.getCell(columnIndex); labelProvider.update(cell); if (!matchesColumnFilter(cell.getText(), filter)) { return false; } } } } finally { if (dummyItem != null) { dummyItem.dispose(); grid.setRedraw(true); } } return true; } protected boolean isApplicable(Viewer viewer) { return getFilteringMode() == feature; } /** * Test whether the given value/filter combination matches. * * @param value the value to test * @param filterValue the filter value * @return true if the value matches the filter value */ protected abstract boolean matchesColumnFilter(Object value, Object filterValue); } /** * Viewer filter for column simple filter support. * * @author Mat Hansen * */ private class GridColumnFilterViewerFilter extends AbstractGridColumnFilterViewerFilter { /** * The Constructor. */ GridColumnFilterViewerFilter() { super(ColumnConfiguration.FEATURE_COLUMN_FILTER); } @Override protected boolean matchesColumnFilter(Object value, Object filterValue) { if (filterValue == null) { return false; } return String.valueOf(value).toLowerCase() .contains(String.valueOf(filterValue).toLowerCase()); } } /** * Viewer filter for column regular expression filter support. */ private class GridColumnRegexFilterViewerFilter extends AbstractGridColumnFilterViewerFilter { private Object rawFilter; private Pattern pattern; /** * Initializes me. */ GridColumnRegexFilterViewerFilter() { super(ColumnConfiguration.FEATURE_COLUMN_REGEX_FILTER); } @Override protected boolean matchesColumnFilter(Object value, Object filterValue) { if (!Objects.equals(filterValue, rawFilter)) { // Cache a new pattern, if possible pattern = parse(String.valueOf(filterValue)); } if (pattern == null) { // Couldn't parse it. Everything will match (user should be able to see examples // in the data to formulate a pattern) return true; } return pattern.matcher(String.valueOf(value)).find(); } protected Pattern parse(String regex) { Pattern result = null; try { result = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); } catch (final PatternSyntaxException e) { // This is normal while the user is formulating the pattern } return result; } } @Override public void setCompareColumn(int columnIndex) { // Reset other columns to avoid left over sort indicators for (final int index : sortableColumns) { final GridColumn column = getTableViewer().getGrid().getColumns()[index]; if (index != columnIndex && column.getSort() != SWT.NONE) { column.setSort(SWT.NONE); } } comparator.setColumn(columnIndex); final GridColumn tableColumn = getTableViewer().getGrid().getColumns()[columnIndex]; tableColumn.setSort(comparator.getDirection()); gridTableViewer.refresh(); } }