Skip to content

Content of file GridPasteKeyListener.java

/*******************************************************************************
 * Copyright (c) 2011-2016 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
 ******************************************************************************/
package org.eclipse.emf.ecp.view.internal.table.nebula.grid;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.eclipse.core.databinding.observable.IObserving;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.ecore.EEnum;
import org.eclipse.emf.ecore.EEnumLiteral;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EStructuralFeature.Setting;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecp.view.spi.model.VControl;
import org.eclipse.emf.ecp.view.spi.model.VDomainModelReference;
import org.eclipse.emf.ecp.view.spi.model.VViewPackage;
import org.eclipse.emf.ecp.view.spi.table.model.VTableControl;
import org.eclipse.emfforms.spi.common.converter.EStructuralFeatureValueConverterService;
import org.eclipse.emfforms.spi.common.validation.PreSetValidationService;
import org.eclipse.emfforms.spi.core.services.databinding.emf.EMFFormsDatabindingEMF;
import org.eclipse.emfforms.spi.localization.EMFFormsLocalizationService;
import org.eclipse.emfforms.spi.swt.table.TableConfiguration;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.nebula.widgets.grid.Grid;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;

/**
 * {@link KeyListener} for the paste action on a {@link Grid} control.
 *
 * @author Alexandra Buzila
 * @author Mathias Schaefer
 * @since 1.10
 *
 */
public class GridPasteKeyListener implements KeyListener {

	private static final String TAB = "\t"; //$NON-NLS-1$
	private static final String NT = "\n\t"; //$NON-NLS-1$
	private static final String IS_INPUTTABLE = "isInputtable"; //$NON-NLS-1$
	private final Clipboard clipboard;
	private final EMFFormsDatabindingEMF dataBinding;
	private final EStructuralFeatureValueConverterService converterService;
	private final VControl vControl;

	private boolean selectPastedCells = true;
	private boolean alreadyPasted;
	private final PreSetValidationService preSetValidationService;
	private final Display display;
	private final EMFFormsLocalizationService localizationService;

	/**
	 * Constructor.
	 *
	 * @param display the {@link Display} on which to allocate this command's {@link Clipboard}.
	 * @param vControl the {@link VTableControl}.
	 * @param dataBinding {@link EMFFormsDatabindingEMF}
	 * @param converterService {@link EStructuralFeatureValueConverterService}
	 * @param localizationService {@link EMFFormsLocalizationService}
	 * @param selectPastedCells whether to select the pasted cells
	 */
	public GridPasteKeyListener(Display display, VControl vControl, EMFFormsDatabindingEMF dataBinding,
		EStructuralFeatureValueConverterService converterService, EMFFormsLocalizationService localizationService,
		boolean selectPastedCells) {
		this.display = display;
		this.localizationService = localizationService;
		clipboard = new Clipboard(display);
		this.vControl = vControl;
		this.dataBinding = dataBinding;
		this.converterService = converterService;
		this.selectPastedCells = selectPastedCells;

		final BundleContext bundleContext = FrameworkUtil
			.getBundle(getClass())
			.getBundleContext();

		final ServiceReference<PreSetValidationService> serviceReference = bundleContext
			.getServiceReference(PreSetValidationService.class);

		preSetValidationService = serviceReference != null ? bundleContext.getService(serviceReference) : null;
	}

	@Override
	public void keyPressed(KeyEvent e) {
		if ((e.stateMask & SWT.CTRL) != 0 && e.keyCode == 'v') {
			if (!alreadyPasted) {
				final Grid grid = (Grid) e.widget;
				final Object contents = clipboard.getContents(TextTransfer.getInstance());
				if (contents instanceof String) {
					pasteSelection(grid, (String) contents);
				}
				alreadyPasted = true;
			}
		} else {
			alreadyPasted = false;
		}
	}

	@Override
	public void keyReleased(KeyEvent e) {
		/* no op */
	}

	/**
	 * Pastes the given contents in the grid.
	 *
	 * @param grid the target {@link Grid}
	 * @param contents the contents to paste
	 */
	public void pasteSelection(Grid grid, String contents) {

		if (grid.getCellSelection().length == 0 || !getControl().isEffectivelyEnabled()
			|| getControl().isEffectivelyReadonly()) {
			return;
		}

		final List<Point> pastedCells = new ArrayList<Point>();
		if (grid.getCellSelection().length > 1 /* more than one cell selected */ &&
			new StringTokenizer(contents, NT, false).countTokens() == 1 /* contents are on one line */ &&
			fillSelectionWithMultipleCopies(grid.getCellSelection(), contents)) {

			// fill selection
			for (final Point startItem : getFillStartPoints(grid.getCellSelection())) {
				pastedCells.addAll(pasteContents(startItem, grid, contents));
			}

		} else {

			// expand selection
			final Point startItem = grid.getCellSelection()[0];
			pastedCells.addAll(pasteContents(startItem, grid, contents));
		}

		if (selectPastedCells && !pastedCells.isEmpty() && grid.isCellSelectionEnabled()) {
			grid.setCellSelection(pastedCells.toArray(new Point[] {}));
		}

	}

	/**
	 * Extract the start points for filling paste from the grid's cell selection.
	 *
	 * @param cellSelection the cell selection
	 * @return the start points
	 */
	static Point[] getFillStartPoints(Point[] cellSelection) {
		final Map<Integer, Set<Integer>> rowToSelectedColumns = createSelectionMap(cellSelection);
		final Point[] result = new Point[rowToSelectedColumns.size()];

		final Set<Integer> columns = rowToSelectedColumns.values().iterator().next();
		final int startColumn = Collections.min(columns);

		int i = 0;
		for (final Integer startRow : rowToSelectedColumns.keySet()) {
			result[i++] = new Point(startColumn, startRow);
		}

		return result;
	}

	/**
	 * Whether the paste logic which will paste the same contents in every selected row is applicable.
	 * Precondition is that multiple cells are selected and contents does not contain a new line.
	 *
	 * @param cellSelection the selected cells
	 * @param contents the contents to paste
	 * @return <code>true</code> if paste should happen, <code>false</code> otherwise
	 */
	static boolean fillSelectionWithMultipleCopies(Point[] cellSelection, String contents) {
		/* build up data struc to analyse selection */
		final Map<Integer, Set<Integer>> rowToSelectedColumns = createSelectionMap(cellSelection);

		/* multiple rows have to be selected */
		if (rowToSelectedColumns.size() < 2) {
			return false;
		}

		/* column selection has to be uniform */
		final Iterator<Set<Integer>> columnSetInterator = rowToSelectedColumns.values().iterator();
		final Set<Integer> referenceSet = columnSetInterator.next();
		while (columnSetInterator.hasNext()) {
			final Set<Integer> next = columnSetInterator.next();
			if (!referenceSet.equals(next)) {
				return false;
			}
		}

		/* if only one column selected, we are fine */
		if (referenceSet.size() == 1) {
			return true;
		}

		/* otherwise selected column count and pasted column count has to match */
		if (contents.split(TAB).length != referenceSet.size()) {
			return false;
		}

		/* and selected columns have to lie next to each other */
		final ArrayList<Integer> selectedColumnIndices = new ArrayList<Integer>(referenceSet);
		Collections.sort(selectedColumnIndices);
		final Iterator<Integer> selectedColumnIndicesIterator = selectedColumnIndices.iterator();
		Integer ref = selectedColumnIndicesIterator.next();
		while (selectedColumnIndicesIterator.hasNext()) {
			if (++ref != selectedColumnIndicesIterator.next()) {
				return false;
			}
		}

		/* all fine */
		return true;
	}

	private static Map<Integer, Set<Integer>> createSelectionMap(Point[] cellSelection) {
		final Map<Integer, Set<Integer>> rowToSelectedColumns = new LinkedHashMap<Integer, Set<Integer>>();
		for (final Point point : cellSelection) {
			if (!rowToSelectedColumns.containsKey(point.y)) {
				rowToSelectedColumns.put(point.y, new LinkedHashSet<Integer>());
			}
			rowToSelectedColumns.get(point.y).add(point.x);
		}
		return rowToSelectedColumns;
	}

	/**
	 * Performs the paste operation.
	 *
	 * @param startItem the start item
	 * @param grid the grid
	 * @param contents the pasted contents
	 * @return the pasted cells
	 */
	// BEGIN COMPLEX CODE
	@SuppressWarnings("restriction")
	public List<Point> pasteContents(Point startItem, Grid grid, String contents) {
		final int startColumn = startItem.x;
		final int startRow = startItem.y;

		final List<Point> pastedCells = new ArrayList<Point>();
		final List<String> invalidValues = new ArrayList<String>();
		int relativeRow = 0;
		final String[] rows = contents.split("\r\n|\n", -1); //$NON-NLS-1$

		prePasteContents();

		try {
			for (final String row : rows) {

				int relativeColumn = 0;

				for (final String cellValueSplit : row.split(TAB, -1)) {

					final String cellValue = modifyCellValue(cellValueSplit);

					final int insertionColumnIndex = startColumn + relativeColumn;
					final int insertionRowIndex = startRow + relativeRow;

					if (insertionColumnIndex >= grid.getColumnCount()) {
						relativeColumn++;
						continue;
					}

					final VDomainModelReference dmr = (VDomainModelReference) grid.getColumn(insertionColumnIndex)
						.getData(TableConfiguration.DMR);

					if (dmr == null || getControl() instanceof VTableControl
						&& org.eclipse.emf.ecp.view.internal.table.swt.TableConfigurationHelper
							.isReadOnly((VTableControl) getControl(), dmr)) {
						relativeColumn++;
						continue;
					}

					if (insertionRowIndex < grid.getItemCount()) {

						final EObject eObject = (EObject) grid.getItem(insertionRowIndex).getData();

						if (isEObjectReadOnly(eObject)) {
							continue;
						}

						IObservableValue value = null;
						try {
							value = dataBinding.getObservableValue(dmr, eObject);
							final EStructuralFeature feature = (EStructuralFeature) value.getValueType();
							final Object convertedValue = getConverterService().convertToModelValue(eObject,
								feature, cellValue);

							if (isSettingReadOnly(eObject, feature, convertedValue)) {
								continue;
							}

							boolean valid = convertedValue != null;

							if (preSetValidationService != null) {
								final Map<Object, Object> context = new LinkedHashMap<Object, Object>();
								context.put("rootEObject", IObserving.class.cast(value).getObserved());//$NON-NLS-1$
								final Diagnostic diag = preSetValidationService.validate(
									feature, valid ? convertedValue : cellValue, context);
								valid = diag.getSeverity() == Diagnostic.OK;
								if (!valid) {
									invalidValues.add(extractDiagnosticMessage(diag, feature, cellValue));
								}
							}

							final EObject observedEobject = (EObject) ((IObserving) value).getObserved();
							final Setting setting = ((InternalEObject) observedEobject).eSetting(feature);
							if (!canBePasted(feature, cellValue, eObject, setting)) {
								invalidValues.add(cellValue);
							} else if (valid) {
								setValue(value, convertedValue);
								pastedCells.add(new Point(insertionColumnIndex, insertionRowIndex));
							}
						}
						// BEGIN SUPRESS CATCH EXCEPTION
						catch (final Exception ex) {// END SUPRESS CATCH EXCEPTION
							// silently ignore this
						} finally {
							if (value != null) {
								value.dispose();
							}
						}

					}
					relativeColumn++;
				}
				relativeRow++;
			}
		} finally {
			postPasteContents();
		}

		showErrors(invalidValues);

		return pastedCells;
	}
	// END COMPLEX CODE

	/**
	 * This method gets called by {@link #pasteContents(Point, Grid, String)} before it will start to loop over the to
	 * be pasted values and begins the pasting.
	 * Clients may use this to notify the user about the paste, setting global variables, showing progress, etc.
	 */
	protected void prePasteContents() {
		/* default implementation does nothing */
	}

	/**
	 * This method get called by {@link #pasteContents(Point, Grid, String)} directely after the paste is done but
	 * before {@link #showErrors(List)} is called. Please note that the call comes from a finally block meaning that the
	 * paste process may have been stoped by an unhandled exception beforehand.
	 * Clients may use this clean up things done in {@link #prePasteContents()} for example.
	 */
	protected void postPasteContents() {
		/* default implementation does nothing */
	}

	/**
	 * This method is called by {@link #pasteContents(Point, Grid, String)} with the determined value for a cell.
	 * Clients may override this method in order to trim the string or change its formatting.
	 *
	 * @param cellValueSplit the cell value determined from the clipboard for the cell.
	 * @return the string to use
	 */
	protected String modifyCellValue(String cellValueSplit) {
		return cellValueSplit;
	}

	/**
	 * Called by {@link #pasteContents(Point, Grid, String)} to determine whether the values of an EObject should be
	 * changed at all.
	 *
	 * @param eObject the EObject in question
	 * @return <code>true</code> if paste for this object should be skipped, <code>false</code> otherwise
	 */
	protected boolean isEObjectReadOnly(EObject eObject) {
		return false;
	}

	/**
	 * Called by {@link #pasteContents(Point, Grid, String)} to determine whether a setting should be changed at all.
	 *
	 * @param eObject the EObject in question
	 * @param feature the Feature in question
	 * @param convertedValue the converted cell value
	 * @return <code>true</code> if paste for this setting should be skipped, <code>false</code> otherwise
	 */
	protected boolean isSettingReadOnly(EObject eObject, EStructuralFeature feature, Object convertedValue) {
		return false;
	}

	/**
	 * Shows the errors to the user.
	 *
	 * @param msgs the collected messages, may be empty if no errors
	 */
	protected void showErrors(List<String> msgs) {
		if (!msgs.isEmpty()) {
			showDialog(
				display.getActiveShell(),
				localizationService.getString(FrameworkUtil.getBundle(getClass()), "InvalidPaste.Title"), //$NON-NLS-1$
				localizationService.getString(FrameworkUtil.getBundle(getClass()), "InvalidPaste.Message"), //$NON-NLS-1$
				msgs);
		}
	}

	/**
	 * Checks whether a given value may be pasted.
	 *
	 * @param feature the feature of the {@code eObject}
	 * @param cellValue the cell value to be pasted
	 * @param eObject the parent {@link EObject}
	 * @param setting {@link Setting}
	 * @return {@code true}, if the value may be pasted, {@code false} otherwise
	 */
	protected boolean canBePasted(EStructuralFeature feature, String cellValue,
		EObject eObject, Setting setting) {

		if (!EEnum.class.isInstance(feature.getEType())) {
			return true;
		}

		final EEnum eEnum = (EEnum) feature.getEType();
		for (final EEnumLiteral literal : eEnum.getELiterals()) {
			final String isInputtable = EcoreUtil.getAnnotation(literal, VViewPackage.NS_URI_170,
				IS_INPUTTABLE);

			if (literal.getLiteral().equals(cellValue) && isInputtable != null) {
				return Boolean.getBoolean(isInputtable);
			}
		}

		return true;
	}

	/**
	 * Sets the given converted value on the observable value.
	 *
	 * @param value the observable value
	 * @param convertedValue the converted value
	 */
	protected void setValue(IObservableValue value, final Object convertedValue) {
value.setValue(convertedValue); } /** * Creates the message for the given {@link Diagnostic} which will be displayed to the user. * * @param diag the diagnostic with the original error message * @param feature the validated feature * @param value the validated value * @return the display string */ protected String extractDiagnosticMessage(Diagnostic diag, EStructuralFeature feature, String value) { return diag.getChildren().get(0).getMessage(); } private static void showDialog(Shell shell, String title, String msg, List<String> warnings) { final StringBuilder builder = new StringBuilder(); builder.append(msg); for (final String warning : warnings) { builder.append("- " + warning) //$NON-NLS-1$ .append("\n"); //$NON-NLS-1$ } MessageDialog.openWarning(shell, title, builder.toString()); } /** * * @return the {@link EStructuralFeatureValueConverterService} */ protected EStructuralFeatureValueConverterService getConverterService() { return converterService; } /** * @return the {@link VControl} */ protected VControl getControl() { return vControl; } }