Skip to content

Content of file ItemProviderEnumCellEditor.java

/*******************************************************************************
 * Copyright (c) 2017-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:
 * Edgar Mueller - initial API and implementation
 * Christian W. Damus - rework for i18n support and code cleanup
 * Lucas Koehler - rework integration
 ******************************************************************************/
package org.eclipse.emf.ecp.view.spi.table.swt;

import static java.lang.Math.min;
import static org.eclipse.emf.ecp.view.internal.table.swt.FigureUtilities.getTextWidth;

import java.text.MessageFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.observable.Observables;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.ValueDiff;
import org.eclipse.core.databinding.property.INativePropertyListener;
import org.eclipse.core.databinding.property.ISimplePropertyListener;
import org.eclipse.core.databinding.property.value.IValueProperty;
import org.eclipse.core.databinding.property.value.SimpleValueProperty;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.emf.common.util.Enumerator;
import org.eclipse.emf.databinding.IEMFObservable;
import org.eclipse.emf.ecore.EAttribute;
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.ecp.common.spi.EMFUtils;
import org.eclipse.emf.ecp.edit.spi.swt.table.ECPEnumCellEditor;
import org.eclipse.emf.ecp.view.internal.core.swt.MatchItemComboViewer;
import org.eclipse.emf.ecp.view.spi.context.ViewModelContext;
import org.eclipse.emf.edit.provider.IItemPropertyDescriptor;
import org.eclipse.emf.edit.provider.IItemPropertySource;
import org.eclipse.emfforms.spi.common.BundleResolver;
import org.eclipse.emfforms.spi.common.BundleResolver.NoBundleFoundException;
import org.eclipse.emfforms.spi.common.BundleResolverFactory;
import org.eclipse.emfforms.spi.common.report.AbstractReport;
import org.eclipse.emfforms.spi.common.report.ReportService;
import org.eclipse.emfforms.spi.localization.EMFFormsLocalizationService;
import org.eclipse.jface.databinding.viewers.ViewerProperties;
import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ColumnViewerEditorActivationEvent; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CCombo; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.osgi.framework.Bundle; /** * Generic {@link org.eclipse.emf.ecp.edit.spi.swt.table.ECPCellEditor ECPCellEditor} which is * applicable for all {@link EAttribute EAttributes} with a Single {@link EEnum} data type. * This cell editor uses the EMF.Edit item provider to determine * the model's proper choice of values for an {@link EEnum} attribute. Additionally, it filters out enum literals with * are marked as <code>isInputtable=false</code> with a custom annotation. * * @author Christian W. Damus * @author Lucas Koehler * @since 1.22 */ public class ItemProviderEnumCellEditor extends ECPEnumCellEditor { /** * Template to generate the localization key for an enum value. First parameter is the EEnum's type name, second * parameter is the value's name. */ private static final String LOCALIZATION_KEY_TEMPLATE = "_UI_%s_%s_literal"; //$NON-NLS-1$ private EMFFormsLocalizationService l10n; private MatchItemComboViewer viewer; private int minWidth; private EAttribute attribute; private BundleResolver bundleResolver = BundleResolverFactory.createBundleResolver(); /** The edit bundle for the EEnum renderered by this cell editor. */ private Optional<Bundle> editBundle; private Optional<EObject> source = Optional.empty(); /** * Initializes me with my parent. * * @param parent my parent composite */ public ItemProviderEnumCellEditor(Composite parent) { super(parent); } /** * Initializes me with my parent and style. * * @param parent my parent composite * @param style my style bits */ public ItemProviderEnumCellEditor(Composite parent, int style) { super(parent, style); } /** * Initializes me with my parent, style, and custom {@link BundleResolver} and {@link EMFFormsLocalizationService}. * * @param parent my parent composite * @param style my style bits * @param bundleResolver custom {@link BundleResolver} * @param l10n custom {@link EMFFormsLocalizationService} */ public ItemProviderEnumCellEditor(Composite parent, int style, BundleResolver bundleResolver, EMFFormsLocalizationService l10n) { this(parent, style); this.bundleResolver = bundleResolver; this.l10n = l10n; } @Override @SuppressWarnings("rawtypes") public UpdateValueStrategy getModelToTargetStrategy(DataBindingContext databindingContext) { return new UpdateValueStrategy() { @Override public Object convert(Object value) { if (!source.isPresent()) { // Extract the source model element from the data binding source = inferSource(databindingContext); } return value; } }; } @Override @SuppressWarnings("rawtypes") public UpdateValueStrategy getTargetToModelStrategy(DataBindingContext databindingContext) { return new UpdateValueStrategy(); } @Override protected Control createControl(Composite parent) { viewer = new MatchItemComboViewer(new CCombo(parent, SWT.NONE)) { @Override public void onEnter() { super.onEnter(); applySelection(); focusLost(); } @Override protected void onEscape() { fireCancelEditor(); } }; final CCombo combo = viewer.getCCombo(); GridDataFactory.fillDefaults().grab(true, false).applyTo(combo); viewer.setContentProvider(ArrayContentProvider.getInstance()); viewer.setLabelProvider(new EnumLabelProvider()); combo.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { applySelection(); } @Override public void focusGained(FocusEvent e) { // nothing to do here } }); return combo; } private void applySelection() { final CCombo combo = viewer.getCCombo(); final int selection = combo.getSelectionIndex(); if (selection >= 0) { final List<?> input = (List<?>) viewer.getInput(); viewer.setSelection(new StructuredSelection(input.get(selection))); } } /** * Gets the proper choice of values provided by the item-provider * of the source object of our data binding for the attribute that * is bound. In addition, we remove all choices annotated by our custom annotation. * <br/> * If the property descriptor is not available or does not return any choice, the enum's literals minus the * annotated ones are returned. * * @return The available enum values, might be empty but never <code>null</code> */ protected List<?> getChoiceOfValues() { final Collection<?> providerChoices = getPropertyDescriptor() // if the propertyDescriptor is present, we have a source .map(descriptor -> descriptor.getChoiceOfValues(getSource().get())) .orElse(Collections.emptySet()); final List<Enumerator> result = getELiterals().stream().map(EEnumLiteral::getInstance) .collect(Collectors.toList()); if (!providerChoices.isEmpty()) { result.retainAll(providerChoices); } return result; } @Override public String getFormatedString(Object value) { // If the propertyDescriptor is present, then we have a source return getPropertyDescriptor().map(desc -> desc.getLabelProvider(getSource().get())) .map(lp -> lp.getText(value)) .orElseGet(() -> getLabel((Enumerator) value)); } private String getLabel(Enumerator enumValue) { final String typeName = attribute.getEType().getName(); return editBundle .map(eB -> l10n.getString(eB, String.format(LOCALIZATION_KEY_TEMPLATE, typeName, enumValue.getName()))) .orElse(enumValue.getLiteral()); } @Override public void instantiate(EStructuralFeature feature, ViewModelContext viewModelContext) { if (l10n == null) { l10n = viewModelContext.getService(EMFFormsLocalizationService.class); } attribute = (EAttribute) feature; try { editBundle = Optional.of(bundleResolver.getEditBundle(feature.getEType())); } catch (final NoBundleFoundException ex) { viewModelContext.getService(ReportService.class) .report(new AbstractReport( MessageFormat.format( "No edit bundle was found for EEnum ''{0}''. Hence, its literals cannot be internationalized for feature ''{1}''.", //$NON-NLS-1$ feature.getEType().getName(), feature.getName()), IStatus.WARNING)); editBundle = Optional.empty(); } final List<?> choices = getChoiceOfValues(); viewer.getCCombo().setVisibleItemCount(min(choices.size(), 8)); final Point emptyViewerSize = viewer.getCCombo().computeSize(SWT.DEFAULT, SWT.DEFAULT, true); minWidth = choices.stream() .mapToInt(value -> getTextWidth(getFormatedString(value), viewer.getCCombo().getFont())) .reduce(50, Math::max); minWidth += emptyViewerSize.x; } @SuppressWarnings("rawtypes") @Override public IValueProperty getValueProperty() { return new ComboValueProperty(); } @Override public void activate(ColumnViewerEditorActivationEvent actEvent) { viewer.setInput(getChoiceOfValues()); source.ifPresent(obj -> { viewer.getCCombo().setText(getFormatedString(obj.eGet(attribute))); }); super.activate(actEvent); if (actEvent.eventType == ColumnViewerEditorActivationEvent.KEY_PRESSED) { final CCombo control = (CCombo) getControl(); if (control != null && Character.isLetterOrDigit(actEvent.character)) { viewer.getBuffer().reset(); // key pressed is not fired during activation viewer.getBuffer().addLast(actEvent.character); } } } @Override public void deactivate() { super.deactivate(); // Forget the source previously inferred source = Optional.empty(); } @Override public int getColumnWidthWeight() { return 100; } @Override public int getMinWidth() { return minWidth; } @Override public EEnum getEEnum() { return (EEnum) attribute.getEType(); } @Override public Image getImage(Object value) { return null; } @Override public void setEditable(boolean editable) { viewer.getCCombo().setEnabled(editable); } // Infer the source model element from the EMF binding in the // context that is for our attribute private Optional<EObject> inferSource(DataBindingContext context) { return ((List<?>) context.getBindings()).stream() .map(Binding.class::cast) .map(Binding::getModel) .filter(IEMFObservable.class::isInstance).map(IEMFObservable.class::cast) .filter(obs -> obs.getStructuralFeature() == attribute) .map(IEMFObservable::getObserved) .map(EObject.class::cast) // Can't observe a feature of a non-EObject .findAny(); } /** * @return The current source EObject */ protected Optional<EObject> getSource() { return source; } /** * @return The {@link IItemPropertyDescriptor} descriptor of the current source if it is available */ protected Optional<IItemPropertyDescriptor> getPropertyDescriptor() { return getSource().flatMap(source -> getPropertyDescriptor(source, attribute.getName())); } @Override protected Object doGetValue() { return viewer.getStructuredSelection().getFirstElement(); } @Override protected void doSetValue(Object value) { viewer.setSelection(value == null ? StructuredSelection.EMPTY : new StructuredSelection(value)); } @Override protected void doSetFocus() { final CCombo combo = viewer.getCCombo(); if (combo == null || combo.isDisposed()) { return; } combo.setFocus(); // Remove text selection and move the cursor to the end. final String text = combo.getText(); if (text != null) { combo.setSelection(new Point(text.length(), text.length())); } } /** * Obtains the EMF.Edit property descriptor for the named property of the {@code object}. * * @param object an object * @param propertyName a property to access * @return its descriptor */ static Optional<IItemPropertyDescriptor> getPropertyDescriptor(EObject object, String propertyName) { return EMFUtils.adapt(object, IItemPropertySource.class) .map(propertySource -> propertySource.getPropertyDescriptor(object, propertyName)); } // // Nested types // /** * Label provider for enumeration values. */ private class EnumLabelProvider extends LabelProvider { EnumLabelProvider() { super(); } @Override public String getText(Object element) { return getFormatedString(element); } } /** * Observable value of the combo. */ private class ComboValueProperty extends SimpleValueProperty<Object, Object> { @Override public Object getValueType() { return CCombo.class; } @Override protected Object doGetValue(Object source) { return ItemProviderEnumCellEditor.this.getValue(); } @Override protected void doSetValue(Object source, Object value) { ItemProviderEnumCellEditor.this.doSetValue(value); } @SuppressWarnings("rawtypes") @Override public IObservableValue observe(Object source) { if (source != ItemProviderEnumCellEditor.this) { return Observables.constantObservableValue(null); } return ViewerProperties.singleSelection().observe(viewer); } @Override public INativePropertyListener<Object> adaptListener( ISimplePropertyListener<Object, ValueDiff<? extends Object>> listener) { return null; } } }