blob: 957f5f7c24c4ed5371e9b1e3214f4ed56761caad [file] [log] [blame]
/*******************************************************************************
* 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();
}
}