Skip to content

Content of file TableSelectionCompositeImpl.java

/*******************************************************************************
 * Copyright (c) 2019 Christian W. Damus 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:
 * Christian W. Damus - initial API and implementation
 ******************************************************************************/
package org.eclipse.emfforms.swt.internal.reference.table;

import java.util.Collection;
import java.util.Hashtable;
import java.util.List;

import org.eclipse.core.databinding.observable.Observables;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.value.ComputedValue;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecp.spi.common.ui.composites.SelectModelElementCompositeImpl;
import org.eclipse.emf.ecp.view.spi.context.ViewModelContext;
import org.eclipse.emf.ecp.view.spi.context.ViewModelContextFactory;
import org.eclipse.emf.ecp.view.spi.model.VDomainModelReference;
import org.eclipse.emf.ecp.view.spi.model.VFeatureDomainModelReferenceSegment;
import org.eclipse.emf.ecp.view.spi.model.VFeaturePathDomainModelReference;
import org.eclipse.emf.ecp.view.spi.model.VView;
import org.eclipse.emf.ecp.view.spi.model.VViewFactory;
import org.eclipse.emf.ecp.view.spi.renderer.NoPropertyDescriptorFoundExeption;
import org.eclipse.emf.ecp.view.spi.renderer.NoRendererFoundException;
import org.eclipse.emf.ecp.view.spi.swt.reporting.InvalidGridDescriptionReport;
import org.eclipse.emf.ecp.view.spi.swt.reporting.RenderingFailedReport;
import org.eclipse.emf.ecp.view.spi.table.model.VTableControl;
import org.eclipse.emf.ecp.view.spi.table.model.VTableDomainModelReference;
import org.eclipse.emf.ecp.view.spi.table.swt.TableControlSWTRenderer;
import org.eclipse.emf.ecp.view.spi.util.swt.ImageRegistryService;
import org.eclipse.emf.ecp.view.template.model.VTViewTemplateProvider;
import org.eclipse.emfforms.common.Optional;
import org.eclipse.emfforms.spi.common.report.ReportService;
import org.eclipse.emfforms.spi.core.services.databinding.emf.DomainModelReferenceConverterEMF;
import org.eclipse.emfforms.spi.core.services.databinding.emf.DomainModelReferenceSegmentConverterEMF;
import org.eclipse.emfforms.spi.core.services.databinding.emf.EMFFormsDatabindingEMF;
import org.eclipse.emfforms.spi.core.services.domainexpander.EMFFormsDMRSegmentExpander;
import org.eclipse.emfforms.spi.core.services.editsupport.EMFFormsEditSupport;
import org.eclipse.emfforms.spi.core.services.label.EMFFormsLabelProvider;
import org.eclipse.emfforms.spi.core.services.label.NoLabelFoundException;
import org.eclipse.emfforms.spi.localization.EMFFormsLocalizationService;
import org.eclipse.emfforms.spi.swt.core.layout.GridDescriptionFactory;
import org.eclipse.emfforms.spi.swt.core.layout.SWTGridDescription;
import org.eclipse.emfforms.spi.swt.table.ColumnConfigurationBuilder;
import org.eclipse.emfforms.spi.swt.table.TableViewerCompositeBuilder;
import org.eclipse.emfforms.spi.swt.table.TableViewerSWTBuilder;
import org.eclipse.emfforms.view.spi.multisegment.model.MultiSegmentUtil;
import org.eclipse.emfforms.view.spi.multisegment.model.VMultiDomainModelReferenceSegment;
import org.eclipse.emfforms.view.spi.multisegment.model.VMultisegmentFactory;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.viewers.CellLabelProvider;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceRegistration;

/**
 * Implementation of a table selection composite that is described by a view model.
 * 
 * @since 1.22
 */
public class TableSelectionCompositeImpl extends SelectModelElementCompositeImpl {

	/** Namespace URI of the dynamic selection extent package. */
	private static final String SELECTION_NS_URI = "http://www.eclipse.org/emf/ecp/view/reference/table/selection"; //$NON-NLS-1$

	/** The selection extent package. */
	private static final EPackage SELECTION_PACKAGE = EPackage.Registry.INSTANCE.getEPackage(SELECTION_NS_URI);

	/** The <em>Selection</em> class. */
	private static final EClass SELECTION_CLASS = (EClass) SELECTION_PACKAGE.getEClassifier("Selection"); //$NON-NLS-1$

	/** The <em>extent</em> reference of the <em>Selection</em> class. */
	private static final EStructuralFeature SELECTION_EXTENT = SELECTION_CLASS.getEStructuralFeature("extent"); //$NON-NLS-1$

	private final Collection<? extends EObject> extent;
	private final VTableControl tableControl;
	private final EObject owner;

	private ViewModelContext context;
	/** Representation of the real DMR which can be used to get labels for the referenced feature. */
	private VDomainModelReference realDMR;
	private ServiceRegistration<DomainModelReferenceConverterEMF> dmrConverter;
	private ServiceRegistration<DomainModelReferenceSegmentConverterEMF> segmentConverter;
	private ServiceRegistration<EMFFormsDMRSegmentExpander> segmentExpander;

	/**
	 * Initializes me with the table view model to render.
	 *
	 * @param extent the collection of objects from which to make a selection
	 * @param tableControl the view model description of the table
	 * @param owner the owner of the {@code reference} being edited
	 * @param reference the reference being edited
	 */
	public TableSelectionCompositeImpl(Collection<? extends EObject> extent, VTableControl tableControl,
		EObject owner, EReference reference) {

		super(extent, reference.isMany());

		this.extent = extent;
		this.tableControl = tableControl;
		this.owner = owner;
	}

	@Override
	public void dispose() {
		if (context != null) {
			context.dispose();
			context = null;
		}

		if (dmrConverter != null) {
			dmrConverter.unregister();
		}

		if (segmentConverter != null) {
			segmentConverter.unregister();
		}
		if (segmentExpander != null) {
			segmentExpander.unregister();
		}

		super.dispose();
	}

	@Override
	protected TableViewer createViewer(Composite composite) {
		final EObject selectionExtent = createSelectionExtent();

		// Put the table in a view by itself
		final VView tableView = VViewFactory.eINSTANCE.createView();
		tableView.getChildren().add(tableControl);

		// We don't want anything in this view to be editable: it's only for selection
		tableView.setReadonly(true);

		// Set the extent DMR into the table model
		if (tableControl.getDomainModelReference() == null) {
			final VFeaturePathDomainModelReference extentDMR = createLegacyExtentDmr(selectionExtent);
			// So we have no column DMRs. What then is the point?
			realDMR = tableControl.getDomainModelReference();
			tableControl.setDomainModelReference(extentDMR);
			registerDMRConverter(selectionExtent, extentDMR);
		} else if (!tableControl.getDomainModelReference().getSegments().isEmpty()) {
			// Segment based table dmr must have a multi segment
			final VMultiDomainModelReferenceSegment realMultiSeg = MultiSegmentUtil
				.getMultiSegment(tableControl.getDomainModelReference())
				.orElseThrow(() -> new IllegalArgumentException(
					"the table model's dmr is segment based but doesn't have a multi segment.")); //$NON-NLS-1$
			adaptRealSegmentDmr(realMultiSeg);
			// Create extent dmr and register necessary services
			final VDomainModelReference ex = configureSegmentExtentDmr(selectionExtent, realMultiSeg);
			tableControl.setDomainModelReference(ex);
		} else if (tableControl.getDomainModelReference() instanceof VTableDomainModelReference) {
			final VFeaturePathDomainModelReference extentDMR = createLegacyExtentDmr(selectionExtent);
			final VTableDomainModelReference dmr = (VTableDomainModelReference) tableControl.getDomainModelReference();
			realDMR = dmr.getDomainModelReference();
			dmr.setDomainModelReference(extentDMR);
			registerDMRConverter(selectionExtent, extentDMR);
		} else {
			throw new IllegalArgumentException("table model has no table DMR"); //$NON-NLS-1$
		}

		context = ViewModelContextFactory.INSTANCE.createViewModelContext(tableView, owner);

		final PrivateTableRenderer renderer = new PrivateTableRenderer(context, tableControl);

		final Control control = renderer.render(composite);

		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).minSize(0, 150).span(2, 1)
			.applyTo(control);

		final TableViewer result = renderer.getTableViewer();

		return result;
	}

	private VFeaturePathDomainModelReference createLegacyExtentDmr(final EObject selectionExtent) {
		final VFeaturePathDomainModelReference extentDMR = VViewFactory.eINSTANCE
			.createFeaturePathDomainModelReference();
		extentDMR.setDomainModelEFeature(selectionExtent.eClass().getEReferences().get(0));
		return extentDMR;
	}

	/**
	 * Replaces the original DMRs multi segment with a feature segment to allow databinding for single references. A
	 * feature segment is sufficient as the dmr only needs to be resolved for labels.
	 *
	 * @param realMultiSeg The multi segment of the original table dmr in the view model
	 */
	private void adaptRealSegmentDmr(final VMultiDomainModelReferenceSegment realMultiSeg) {
		realDMR = tableControl.getDomainModelReference();
		// replace multi with feature segment to avoid failed databinding for single references.
		realDMR.getSegments().remove(realMultiSeg);
		final VFeatureDomainModelReferenceSegment realReplace = VViewFactory.eINSTANCE
			.createFeatureDomainModelReferenceSegment();
		realReplace.setDomainModelFeature(realMultiSeg.getDomainModelFeature());
		realDMR.getSegments().add(realReplace);
	}

	/**
	 * Create segment based extent dmr whose multi segment contains a copy of all real child dmrs. Also register
	 * necessary services.
	 *
	 * @param selectionExtent The selection extent {@link EObject}
	 * @param realMultiSegment The multi segment of the original table dmr in the view model
	 * @return The extent segment DMR
	 */
	private VDomainModelReference configureSegmentExtentDmr(EObject selectionExtent,
		VMultiDomainModelReferenceSegment realMultiSegment) {
		final VDomainModelReference extentDmr = VViewFactory.eINSTANCE.createDomainModelReference();
		final VMultiDomainModelReferenceSegment extentSegment = VMultisegmentFactory.eINSTANCE
			.createMultiDomainModelReferenceSegment();
		final EReference extentRef = selectionExtent.eClass().getEReferences().get(0);
		extentSegment.setDomainModelFeature(extentRef.getName());
		// This moves the child DMRs but we do not need them in the "real" DMR. We need the original ones so they
		// are still found by enablement configurations used to name the columns
		extentSegment.getChildDomainModelReferences().addAll(realMultiSegment.getChildDomainModelReferences());
		extentDmr.getSegments().add(extentSegment);

		// Configure services for the extent segment
		registerSegmentServices(selectionExtent, extentSegment, extentRef);

		return extentDmr;
	}

	/**
	 * Create a column label provider that delegates to another label provider.
	 *
	 * @param delegate the label provider delegate
	 * @return the column label provider
	 */
	protected CellLabelProvider createColumnLabelProvider(ILabelProvider delegate) {
		return new ColumnLabelProvider() {
			@Override
			public String getText(Object element) {
				return delegate.getText(element);
			}

			@Override
			public Image getImage(Object element) {
				return delegate.getImage(element);
			}
		};
	}

	private EObject createSelectionExtent() {
		final EObject result = SELECTION_PACKAGE.getEFactoryInstance().create(SELECTION_CLASS);
		@SuppressWarnings("unchecked")
		final EList<EObject> selectionExtent = (EList<EObject>) result.eGet(SELECTION_EXTENT);
		selectionExtent.addAll(extent);

		return result;
	}

	private void registerDMRConverter(EObject source, VFeaturePathDomainModelReference dmr) {
		final DelegatingDomainModelReferenceConverter delegator = new DelegatingDomainModelReferenceConverter(dmr,
			__ -> source);
		final BundleContext ctx = FrameworkUtil.getBundle(TableSelectionCompositeImpl.class).getBundleContext();

		final Hashtable<String, Object> properties = new Hashtable<>();
		properties.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
		dmrConverter = ctx.registerService(DomainModelReferenceConverterEMF.class, delegator, properties);
	}

	private void registerSegmentServices(EObject source, VFeatureDomainModelReferenceSegment segment,
		EStructuralFeature segmentFeature) {

		final DelegatingDmrSegmentConverter delegator = new DelegatingDmrSegmentConverter(segment, segmentFeature,
			__ -> source);
		final DummyDomainExpander expander = new DummyDomainExpander(segment);
		final BundleContext ctx = FrameworkUtil.getBundle(TableSelectionCompositeImpl.class).getBundleContext();

		final Hashtable<String, Object> properties = new Hashtable<>();
		properties.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
		segmentConverter = ctx.registerService(DomainModelReferenceSegmentConverterEMF.class, delegator, properties);
		segmentExpander = ctx.registerService(EMFFormsDMRSegmentExpander.class, expander, properties);
	}

	//
	// Nested types
	//

	/**
	 * Private subclass of the table renderer to access some protected API and customize
	 * its presentation for our use case.
	 */
	private final class PrivateTableRenderer extends TableControlSWTRenderer {

		private final EMFFormsLocalizationService l10n;
		private final EMFFormsLabelProvider labels;

		/**
		 * Initializes me.
		 */
		PrivateTableRenderer(ViewModelContext context, VTableControl vTable) {
			super(vTable, context, context.getService(ReportService.class),
				context.getService(EMFFormsDatabindingEMF.class),
				context.getService(EMFFormsLabelProvider.class),
				context.getService(VTViewTemplateProvider.class),
				context.getService(ImageRegistryService.class),
				context.getService(EMFFormsEditSupport.class),
				context.getService(EMFFormsLocalizationService.class));

			l10n = context.getService(EMFFormsLocalizationService.class);
			labels = context.getService(EMFFormsLabelProvider.class);
		}

		@Override
		protected TableViewer getTableViewer() {
			return (TableViewer) super.getTableViewer();
		}

		@Override
		protected TableViewerCompositeBuilder createTableViewerCompositeBuilder() {
			return new MinimalCompositeBuilder();
		}

		@Override
		protected int addAdditionalColumns(TableViewerSWTBuilder tableViewerSWTBuilder) {
			// Inject the selection column
			final ColumnConfigurationBuilder columnBuilder = ColumnConfigurationBuilder.usingDefaults()
				.labelProvider(createColumnLabelProvider(getLabelProvider()))
				.minWidth(50).weight(100).resizable(true)
				.text(getSelectionColumnTitle())
				.tooltip(getSelectionColumnTooltip());
			tableViewerSWTBuilder.addColumn(columnBuilder.build());

			return 1;
		}

		private IObservableValue<String> getSelectionColumnTitle() {
			final String result = l10n.getString(TableSelectionCompositeImpl.class, "selectionColumn"); //$NON-NLS-1$
			return Observables.constantObservableValue(Realm.getDefault(), result, String.class);
		}

		private IObservableValue<String> getSelectionColumnTooltip() {
			final IObservableValue<String> featureName = getRealDMRDisplayName();
			return ComputedValue
				.create(() -> NLS.bind(l10n.getString(TableSelectionCompositeImpl.class, "selectionTooltip"), //$NON-NLS-1$
					featureName.getValue()));
		}

		private IObservableValue<String> getRealDMRDisplayName() {
			IObservableValue<String> result;
			try {
				result = labels.getDisplayName(realDMR, owner);
} catch (final NoLabelFoundException e) { result = Observables.constantObservableValue(Realm.getDefault(), l10n.getString(TableSelectionCompositeImpl.class, "reference"), //$NON-NLS-1$ String.class); } return result; } Control render(Composite parent) { Control result = null; init(); final SWTGridDescription grid = getGridDescription( GridDescriptionFactory.INSTANCE.createEmptyGridDescription()); if (grid.getGrid().isEmpty()) { getReportService().report( new InvalidGridDescriptionReport("grid has no cells")); //$NON-NLS-1$ } try { result = renderTableControl(grid.getGrid().get(0), parent); finalizeRendering(parent); } catch (final NoRendererFoundException | NoPropertyDescriptorFoundExeption e) { getReportService().report(new RenderingFailedReport(e)); } return result; } // // Nested types // /** * A composite builder that leaves the top composite empty. */ private final class MinimalCompositeBuilder extends TableControlSWTRendererCompositeBuilder { @Override protected Label createTitleLabel(Composite parentComposite, Color background) { return null; } @Override protected Label createValidationLabel(Composite topComposite) { return null; } @Override protected Composite createButtonComposite(Composite parentComposite) { return null; } @Override public Optional<Label> getTitleLabel() { return Optional.empty(); } @Override public Optional<List<Control>> getValidationControls() { return Optional.empty(); } @Override public Optional<Composite> getButtonComposite() { return Optional.empty(); } } } }