blob: aaada25f37c548785369e6cbc16543ffd047f4c6 [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:
* Eugen - initial API and implementation
******************************************************************************/
package org.eclipse.emf.ecp.view.spi.core.swt;
import java.util.LinkedHashMap;
import java.util.Map;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.observable.IObserving;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.property.value.IValueProperty;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.emf.databinding.EMFDataBindingContext;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecp.view.spi.context.ViewModelContext;
import org.eclipse.emf.ecp.view.spi.model.LabelAlignment;
import org.eclipse.emf.ecp.view.spi.model.ModelChangeListener;
import org.eclipse.emf.ecp.view.spi.model.ModelChangeNotification;
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.VElement;
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.RenderingFailedReport;
import org.eclipse.emf.ecp.view.template.model.VTViewTemplateProvider;
import org.eclipse.emf.ecp.view.template.style.mandatory.model.VTMandatoryStyleProperty;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emfforms.spi.common.report.AbstractReport;
import org.eclipse.emfforms.spi.common.report.ReportService;
import org.eclipse.emfforms.spi.core.services.databinding.DatabindingFailedException;
import org.eclipse.emfforms.spi.core.services.databinding.DatabindingFailedReport;
import org.eclipse.emfforms.spi.core.services.databinding.EMFFormsDatabinding;
import org.eclipse.emfforms.spi.core.services.label.EMFFormsLabelProvider;
import org.eclipse.emfforms.spi.core.services.label.NoLabelFoundException;
import org.eclipse.emfforms.spi.core.services.structuralchange.EMFFormsStructuralChangeTester;
import org.eclipse.emfforms.spi.core.services.view.RootDomainModelChangeListener;
import org.eclipse.emfforms.spi.swt.core.AbstractSWTRenderer;
import org.eclipse.emfforms.spi.swt.core.EMFFormsControlProcessorService;
import org.eclipse.emfforms.spi.swt.core.SWTDataElementIdHelper;
import org.eclipse.emfforms.spi.swt.core.layout.SWTGridCell;
import org.eclipse.emfforms.spi.swt.core.ui.SWTValidationHelper;
import org.eclipse.emfforms.spi.swt.core.ui.SWTValidationUiService;
import org.eclipse.jface.databinding.swt.WidgetProperties;
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.Display;
import org.eclipse.swt.widgets.Label;
/**
* Super class for all kinds of control renderer.
*
* @param <VCONTROL> the {@link VControl} of this renderer.
* @author Eugen Neufeld
*
*/
public abstract class AbstractControlSWTRenderer<VCONTROL extends VControl> extends AbstractSWTRenderer<VCONTROL>
implements RootDomainModelChangeListener {
private SWTValidationHelper swtValidationHelper = SWTValidationHelper.INSTANCE;
private final EMFFormsDatabinding emfFormsDatabinding;
private final EMFFormsLabelProvider emfFormsLabelProvider;
private final VTViewTemplateProvider vtViewTemplateProvider;
private boolean isDisposed;
private IObservableValue modelValue;
private final Map<Integer, Color> severityBackgroundColorMap = new LinkedHashMap<Integer, Color>();
private final Map<Integer, Color> severityForegroundColorMap = new LinkedHashMap<Integer, Color>();
private final Map<Integer, Image> severityIconMap = new LinkedHashMap<Integer, Image>();
private final SWTValidationUiService validationUiService;
/**
* Default constructor.
*
* @param vElement the view model element to be rendered
* @param viewContext the view context
* @param emfFormsDatabinding The {@link EMFFormsDatabinding}
* @param emfFormsLabelProvider The {@link EMFFormsLabelProvider}
* @param reportService The {@link ReportService}
* @param vtViewTemplateProvider The {@link VTViewTemplateProvider}
* @since 1.6
*/
public AbstractControlSWTRenderer(VCONTROL vElement, ViewModelContext viewContext, ReportService reportService,
EMFFormsDatabinding emfFormsDatabinding, EMFFormsLabelProvider emfFormsLabelProvider,
VTViewTemplateProvider vtViewTemplateProvider) {
this(vElement, viewContext, reportService, emfFormsDatabinding, emfFormsLabelProvider, vtViewTemplateProvider,
viewContext.getService(SWTValidationUiService.class));
}
/**
* Additional constructor allowing to specify a custom {@link SWTValidationHelper}.
*
* @param vElement the view model element to be rendered
* @param viewContext the view context
* @param emfFormsDatabinding The {@link EMFFormsDatabinding}
* @param emfFormsLabelProvider The {@link EMFFormsLabelProvider}
* @param reportService The {@link ReportService}
* @param vtViewTemplateProvider The {@link VTViewTemplateProvider}
* @param swtValidationHelper The {@link SWTValidationHelper}
* @since 1.6
*/
public AbstractControlSWTRenderer(VCONTROL vElement, ViewModelContext viewContext, ReportService reportService,
EMFFormsDatabinding emfFormsDatabinding, EMFFormsLabelProvider emfFormsLabelProvider,
VTViewTemplateProvider vtViewTemplateProvider, SWTValidationHelper swtValidationHelper) {
this(vElement, viewContext, reportService, emfFormsDatabinding, emfFormsLabelProvider, vtViewTemplateProvider);
this.swtValidationHelper = swtValidationHelper;
}
/**
* Default constructor.
*
* @param vElement the view model element to be rendered
* @param viewContext the view context
* @param emfFormsDatabinding The {@link EMFFormsDatabinding}
* @param emfFormsLabelProvider The {@link EMFFormsLabelProvider}
* @param reportService The {@link ReportService}
* @param vtViewTemplateProvider The {@link VTViewTemplateProvider}
* @param validationUiService The {@link SWTValidationUiService}
* @since 1.23
*/
public AbstractControlSWTRenderer(VCONTROL vElement, ViewModelContext viewContext, ReportService reportService,
EMFFormsDatabinding emfFormsDatabinding, EMFFormsLabelProvider emfFormsLabelProvider,
VTViewTemplateProvider vtViewTemplateProvider, SWTValidationUiService validationUiService) {
super(vElement, viewContext, reportService);
this.emfFormsDatabinding = emfFormsDatabinding;
this.emfFormsLabelProvider = emfFormsLabelProvider;
this.vtViewTemplateProvider = vtViewTemplateProvider;
this.validationUiService = validationUiService;
viewModelDBC = new EMFDataBindingContext();
viewContext.registerRootDomainModelChangeListener(this);
isDisposed = false;
}
/**
* The {@link EMFFormsDatabinding} to use.
*
* @return The EMFFormsDatabinding
* @since 1.6
*/
protected EMFFormsDatabinding getEMFFormsDatabinding() {
return emfFormsDatabinding;
}
/**
* The {@link EMFFormsLabelProvider} to use.
*
* @return The EMFFormsLabelProvider
* @since 1.6
*/
protected EMFFormsLabelProvider getEMFFormsLabelProvider() {
return emfFormsLabelProvider;
}
/**
* The {@link VTViewTemplateProvider} to use.
*
* @return The VTViewTemplateProvider
* @since 1.6
*/
protected VTViewTemplateProvider getVTViewTemplateProvider() {
return vtViewTemplateProvider;
}
private DataBindingContext dataBindingContext;
private ModelChangeListener modelChangeListener;
private final EMFDataBindingContext viewModelDBC;
@Override
protected void postInit() {
super.postInit();
modelChangeListener = new ModelChangeListener() {
@Override
public void notifyChange(ModelChangeNotification notification) {
if (isDisposed) {
return;
}
// Execute applyEnable whenever the structure of the VControl's DMR has been changed.
final EMFFormsStructuralChangeTester changeTester = getViewModelContext()
.getService(EMFFormsStructuralChangeTester.class);
if (changeTester.isStructureChanged(getVElement().getDomainModelReference(),
getViewModelContext().getDomainModel(), notification)) {
Display.getDefault().asyncExec(() -> {
if (!isDisposed) {
applyEnable();
}
});
}
}
};
if (getVElement().getDomainModelReference() != null) {
getViewModelContext().registerDomainChangeListener(modelChangeListener);
}
applyEnable();
applyReadOnly();
if (isUnchangeableFeature()) {
applyUnchangeableFeature();
}
}
/**
* Checks if the value referenced by the DMR can be changed or not by the user.
*
* @return <code>true</code> if the value cannot be changed.
*/
protected boolean isUnchangeableFeature() {
final VDomainModelReference ref = getVElement().getDomainModelReference();
if (ref == null) {
getReportService()
.report(new AbstractReport(
String.format("No DomainModelReference could be found for the VElement %1$s.", //$NON-NLS-1$
getVElement().getName()),
IStatus.ERROR));
}
final EObject eObject = getViewModelContext().getDomainModel();
try {
@SuppressWarnings("rawtypes")
final IValueProperty valueProperty = getEMFFormsDatabinding().getValueProperty(ref, eObject);
return !EStructuralFeature.class.cast(valueProperty.getValueType()).isChangeable();
} catch (final DatabindingFailedException ex) {
getReportService().report(new DatabindingFailedReport(ex));
return false;
}
}
@Override
public Control render(SWTGridCell cell, Composite parent)
throws NoRendererFoundException, NoPropertyDescriptorFoundExeption {
final Control control = super.render(cell, parent);
if (control == null) {
return null;
}
if (!canHandleControlProcessor()) {
defaultHandleControlProcessorForCell(control, cell);
}
return control;
}
/**
* This method is applied if the control's feature is configured as unchangeable.
* The renderer is usually disabled when feature is not changeable.
*
* @since 1.20
*/
protected void applyUnchangeableFeature() {
getVElement().setReadonly(true);
}
/**
* <p>
* Indicates if the given Control SWT renderer takes the responsibility to call a possibly existing
* {@link EMFFormsControlProcessorService} itself.
* </p>
* <p>
* The default implementation returns {@code false}.
* </p>
*
* @return
* {@code true} if the Control SWT renderer can handle the {@link EMFFormsControlProcessorService} itself,
* {@code false} otherwise.
* @since 1.8
*/
protected boolean canHandleControlProcessor() {
return false;
}
/**
* This method is called by {link {@link #render(SWTGridCell, Composite)} for each
* {@code control} and {@code cell} if {@link #canHandleControlProcessor()} returns {@code false}. The default
* implementation forwards to {@link #defaultHandleControlProcessor(Control)} if the cell's column is {@code 2}.
*
* @param control
* The {@link Control} which is to be processed by the {@link EMFFormsControlProcessorService}.
* @param cell
* The {@link SWTGridCell} for the given {@code control}.
* @since 1.8
*/
protected void defaultHandleControlProcessorForCell(Control control, SWTGridCell cell) {
if (cell.getColumn() == 2) {
defaultHandleControlProcessor(control);
}
}
/**
* Calls a possibly existing {@link EMFFormsControlProcessorService} for the given {@code control}.
*
* @param control
* The {@link Control} which is to be processed by the {@link EMFFormsControlProcessorService}.
* @since 1.8
*/
protected void defaultHandleControlProcessor(Control control) {
if (getViewModelContext().hasService(EMFFormsControlProcessorService.class)) {
final EMFFormsControlProcessorService service = getViewModelContext()
.getService(EMFFormsControlProcessorService.class);
service.process(control, getVElement(), getViewModelContext());
}
}
@Override
protected void dispose() {
isDisposed = true;
getViewModelContext().unregisterDomainChangeListener(modelChangeListener);
getViewModelContext().unregisterRootDomainModelChangeListener(this);
modelChangeListener = null;
if (dataBindingContext != null) {
dataBindingContext.dispose();
dataBindingContext = null;
}
viewModelDBC.dispose();
if (modelValue != null) {
modelValue.dispose();
}
super.dispose();
}
/**
* Returns the validation icon matching the given severity.
*
* @param severity the severity of the {@link org.eclipse.emf.common.util.Diagnostic}
* @return the icon to be displayed, or <code>null</code> when no icon is to be displayed
* @deprecated use {@link #getValidationIcon()} for default behavior or use the
* {@link SWTValidationUiService} if you need to get the color for a specific diagnostic.
*/
@Deprecated
protected final Image getValidationIcon(int severity) {
if (!severityIconMap.containsKey(severity)) {
final Image validationIcon = swtValidationHelper.getValidationIcon(severity, getVElement(),
getViewModelContext());
severityIconMap.put(severity, validationIcon);
}
return severityIconMap.get(severity);
}
/**
* Returns the validation icon for the current validation result of this control's {@link VElement}.
*
* @return the icon to be displayed, or <code>null</code> when no icon is to be displayed
* @since 1.23
*/
protected final Image getValidationIcon() {
return validationUiService.getValidationIcon(getVElement(), getViewModelContext());
}
/**
* Returns the background color for a control with the given validation severity.
*
* @param severity severity the severity of the {@link org.eclipse.emf.common.util.Diagnostic}
* @return the color to be used as a background color
* @deprecated use {@link #getValidationBackgroundColor()} for default behavior or use the
* {@link SWTValidationUiService} if you need to get the color for a specific diagnostic.
*/
@Deprecated
protected final Color getValidationBackgroundColor(int severity) {
if (isDisposed) {
return null;
}
if (!severityBackgroundColorMap.containsKey(severity)) {
final Color validationBackgroundColor = swtValidationHelper
.getValidationBackgroundColor(severity, getVElement(), getViewModelContext());
severityBackgroundColorMap.put(severity, validationBackgroundColor);
}
return severityBackgroundColorMap.get(severity);
}
/**
* Returns the background color for the current validation result of this control's {@link VElement}.
*
* @return the color to be used as a background color
* @since 1.23
*/
protected final Color getValidationBackgroundColor() {
if (isDisposed) {
return null;
}
return validationUiService.getValidationBackgroundColor(getVElement(), getViewModelContext());
}
/**
* Returns the foreground color for a control with the given validation severity.
*
* @param severity severity the severity of the {@link org.eclipse.emf.common.util.Diagnostic}
* @return the color to be used as a foreground color
* @since 1.10
* @deprecated use {@link #getValidationForegroundColor()} for default behavior or use the
* {@link SWTValidationUiService} if you need to get the color for a specific diagnostic.
*/
@Deprecated
protected final Color getValidationForegroundColor(int severity) {
if (isDisposed) {
return null;
}
if (!severityForegroundColorMap.containsKey(severity)) {
final Color validationForegroundColor = swtValidationHelper
.getValidationForegroundColor(severity, getVElement(), getViewModelContext());
severityForegroundColorMap.put(severity, validationForegroundColor);
}
return severityForegroundColorMap.get(severity);
}
/**
* Returns the foreground color for the current validation result of this control's {@link VElement}.
*
* @return the color to be used as a foreground color
* @since 1.23
*/
protected final Color getValidationForegroundColor() {
if (isDisposed) {
return null;
}
return validationUiService.getValidationForegroundColor(getVElement(), getViewModelContext());
}
/**
* Creates a new {@link DataBindingContext}.
*
* @return a new {@link DataBindingContext} each time this method is called
*/
protected final DataBindingContext getDataBindingContext() {
if (dataBindingContext == null) {
dataBindingContext = new EMFDataBindingContext();
}
return dataBindingContext;
}
/**
* Returns an {@link IObservableValue} based on the control's domain model reference and domain model.
*
* @return the {@link IObservableValue}
* @throws DatabindingFailedException if the databinding of the domain model object fails.
* @since 1.6
*/
protected final IObservableValue getModelValue() throws DatabindingFailedException {
if (modelValue == null) {
final VDomainModelReference ref = getVElement().getDomainModelReference();
if (ref == null) {
throw new DatabindingFailedException(String
.format(
"No DomainModelReference could be found for the VElement %1$s.", getVElement().getName())); //$NON-NLS-1$
}
final EObject eObject = getViewModelContext().getDomainModel();
final EMFFormsDatabinding databindingService = getEMFFormsDatabinding();
modelValue = databindingService.getObservableValue(ref, eObject);
}
return modelValue;
}
/**
* Returns the {@link EditingDomain} for the provided {@link EObject domain model}.
*
* @param domainModel The provided {@link EObject domain model}
* @return The {@link EditingDomain} of this {@link EObject domain model}
* @since 1.6
*/
protected final EditingDomain getEditingDomain(EObject domainModel) {
return AdapterFactoryEditingDomain.getEditingDomainFor(domainModel);
}
/**
* Create the {@link Control} displaying the label of the current {@link VControl}.
*
* @param parent the {@link Composite} to render onto
* @return the created {@link Control} or null
*/
protected Control createLabel(final Composite parent) {
Label label = null;
labelRender: if (hasLeftLabelAlignment()) {
final VDomainModelReference domainModelReference = getVElement().getDomainModelReference();
final IValueProperty valueProperty;
try {
valueProperty = getEMFFormsDatabinding().getValueProperty(domainModelReference,
getViewModelContext().getDomainModel());
} catch (final DatabindingFailedException ex) {
getReportService().report(new RenderingFailedReport(ex));
break labelRender;
} catch (final IllegalArgumentException ex) {
getReportService().report(new AbstractReport(ex));
break labelRender;
}
final EMFFormsLabelProvider labelProvider = getEMFFormsLabelProvider();
label = new Label(parent, getLabelStyleBits());
label.setData(CUSTOM_VARIANT, "org_eclipse_emf_ecp_control_label"); //$NON-NLS-1$
SWTDataElementIdHelper.setElementIdDataWithSubId(label, getVElement(), "control_label", //$NON-NLS-1$
getViewModelContext());
label.setBackground(parent.getBackground());
final EObject rootObject = getViewModelContext().getDomainModel();
try {
final IObservableValue textObservable = WidgetProperties.text().observe(label);
final IObservableValue displayNameObservable = labelProvider.getDisplayName(domainModelReference,
rootObject);
viewModelDBC.bindValue(textObservable, displayNameObservable, null, new UpdateValueStrategy() {
/**
* {@inheritDoc}
*
* @see org.eclipse.core.databinding.UpdateValueStrategy#convert(java.lang.Object)
*/
@Override
public Object convert(Object value) {
String extra = ""; //$NON-NLS-1$
final VTMandatoryStyleProperty mandatoryStyle = getMandatoryStyle();
final EStructuralFeature structuralFeature = (EStructuralFeature) valueProperty.getValueType();
if (mandatoryStyle.isHighliteMandatoryFields() && structuralFeature.getLowerBound() > 0) {
extra = mandatoryStyle.getMandatoryMarker();
}
final String result = (String) super.convert(value);
return result + extra;
}
});
final IObservableValue tooltipObservable = WidgetProperties.tooltipText().observe(label);
final IObservableValue descriptionObservable = labelProvider.getDescription(domainModelReference,
rootObject);
viewModelDBC.bindValue(tooltipObservable, descriptionObservable);
} catch (final NoLabelFoundException e) {
// FIXME Expectations?
getReportService().report(new RenderingFailedReport(e));
}
}
return label;
}
/**
* @return the style bits for the control's label
* @since 1.16
*/
protected int getLabelStyleBits() {
return AbstractControlSWTRendererUtil
.getLabelStyleBits(getVTViewTemplateProvider(), getVElement(), getViewModelContext());
}
/**
* Whether the label for this control should be rendered on the left of the control. This is the case if the
* {@link VControl#getLabelAlignment()} is set to {@link LabelAlignment#LEFT} or {@link LabelAlignment#DEFAULT}.
*
* @return <code>true</code> if label should be on the left, <code>false</code> otherwise
* @since 1.7
*/
protected boolean hasLeftLabelAlignment() {
return getVElement().getLabelAlignment() == LabelAlignment.LEFT
|| getVElement().getLabelAlignment() == LabelAlignment.DEFAULT;
}
private VTMandatoryStyleProperty getMandatoryStyle() {
return AbstractControlSWTRendererUtil
.getMandatoryStyle(vtViewTemplateProvider, getVElement(), getViewModelContext());
}
/**
* Creates a validation icon.
*
* @param composite the {@link Composite} to create onto
* @return the created Label
*/
protected Label createValidationIcon(Composite composite) {
final Label validationLabel = new Label(composite, SWT.NONE);
SWTDataElementIdHelper.setElementIdDataWithSubId(validationLabel, getVElement(), "control_validation", //$NON-NLS-1$
getViewModelContext());
validationLabel.setBackground(composite.getBackground());
return validationLabel;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emfforms.spi.swt.core.AbstractSWTRenderer#applyEnable()
* @since 1.6
*/
@Override
protected void applyEnable() {
for (final SWTGridCell gridCell : getControls().keySet()) {
try {
final boolean observedNotNull = ((IObserving) getModelValue()).getObserved() != null;
final boolean enabled = observedNotNull && getVElement().isEffectivelyEnabled();
setControlEnabled(gridCell, getControls().get(gridCell), enabled);
if (Boolean.FALSE.equals(enabled)) {
getVElement().setDiagnostic(null);
}
} catch (final DatabindingFailedException ex) {
getReportService().report(new DatabindingFailedReport(ex));
setControlEnabled(gridCell, getControls().get(gridCell), false);
getVElement().setDiagnostic(null);
}
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emfforms.spi.core.services.view.RootDomainModelChangeListener#notifyChange()
* @since 1.9
*/
@Override
public void notifyChange() {
// TODO correct? - works so far
if (modelValue != null) {
modelValue.dispose();
modelValue = null;
}
try {
rootDomainModelChanged();
} catch (final DatabindingFailedException ex) {
getReportService().report(new AbstractReport(ex, "Could not process the root domain model change.")); //$NON-NLS-1$
}
}
/**
* This method is called in {@link #notifyChange()} when the root domain model of the view model context changes.
*
* @throws DatabindingFailedException If the databinding failed
* @since 1.9
*/
protected void rootDomainModelChanged() throws DatabindingFailedException {
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emfforms.spi.swt.core.AbstractSWTRenderer#applyReadOnly()
*/
@Override
protected void applyReadOnly() {
for (final SWTGridCell gridCell : getControls().keySet()) {
setControlEnabled(gridCell, getControls().get(gridCell), !getVElement().isEffectivelyReadonly());
}
}
}