Skip to content

Content of file AbstractSWTRenderer.java

/*******************************************************************************
 * 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:
 * Edagr Mueller - initial API and implementation
 * Eugen Neufeld - Refactoring
 * Christian W. Damus - bugs 527686, 548592
 ******************************************************************************/
package org.eclipse.emfforms.spi.swt.core;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.emf.common.util.AbstractTreeIterator;
import org.eclipse.emf.ecp.view.model.common.AbstractRenderer;
import org.eclipse.emf.ecp.view.spi.context.ViewModelContext;
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.VDiagnostic;
import org.eclipse.emf.ecp.view.spi.model.VElement;
import org.eclipse.emf.ecp.view.spi.model.VViewPackage;
import org.eclipse.emf.ecp.view.spi.renderer.NoPropertyDescriptorFoundExeption;
import org.eclipse.emf.ecp.view.spi.renderer.NoRendererFoundException;
import org.eclipse.emfforms.spi.common.report.ReportService;
import org.eclipse.emfforms.spi.swt.core.layout.EMFFormsSWTLayoutUtil;
import org.eclipse.emfforms.spi.swt.core.layout.SWTGridCell;
import org.eclipse.emfforms.spi.swt.core.layout.SWTGridDescription;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;

/**
 * Common base class for all SWT specific renderer classes.
 *
 * A renderer using other renderers to render its contents must call this methods in this order:
 *
 * <pre>
 *  {@link #getGridDescription(SWTGridDescription)}
 *  for each SWTGridCell
 *  	{@link #render(SWTGridCell, Composite)}
 * {@link #finalizeRendering(Composite)}
 * </pre>
 *
 * If you don't call {@link #finalizeRendering(Composite)} after the rendering, the automatic disposing of the renderer
 * will not work, as well as the initial validation check.
 *
 * @author Eugen Neufeld
 *
 * @param <VELEMENT> the actual type of the {@link VElement} to be drawn
 * @since 1.2
 */
public abstract class AbstractSWTRenderer<VELEMENT extends VElement> extends AbstractRenderer<VELEMENT> {

	/**
	 * Variant constant for indicating RAP controls.
	 */
	protected static final String CUSTOM_VARIANT = "org.eclipse.rap.rwt.customVariant"; //$NON-NLS-1$
	private ModelChangeListener listener;
	private Map<SWTGridCell, Control> controls;
	private boolean renderingFinished;

	/**
	 * Default Constructor.
	 *
	 * @param vElement the view element to be rendered
	 * @param viewContext The view model context
	 * @param reportService the ReportService to use
	 * @since 1.6
	 */
	public AbstractSWTRenderer(final VELEMENT vElement, final ViewModelContext viewContext,
		ReportService reportService) {
		super(vElement, viewContext, reportService);
	}

	/**
	 * Returns the GridDescription for this Renderer.
	 *
	 * @param gridDescription the current {@link SWTGridDescription}
	 * @return the number of controls per row
	 * @since 1.3
	 */
	public abstract SWTGridDescription getGridDescription(SWTGridDescription gridDescription);

	/**
	 * Initializes the {@link AbstractSWTRenderer}.
	 *
	 * @since 1.6
	 */
	public final void init() {
		preInit();
		controls = new LinkedHashMap<SWTGridCell, Control>();
		if (getViewModelContext() != null) {
			listener = new ViewChangeListener();
			getViewModelContext().registerViewChangeListener(listener);
		}
		getViewModelContext().addContextUser(this);

		postInit();
	}

	/**
	 * Returns a copy of the {@link GridCell} to {@link Control} map.
	 *
	 * @return a copy of the controls map
	 * @since 1.3
	 */
	protected final Map<SWTGridCell, Control> getControls() {
		if (controls == null) {
			return Collections.emptyMap();
		}
		return new LinkedHashMap<SWTGridCell, Control>(controls);
	}

	/**
	 * Use this method to initialize objects which are needed already before rendering.
	 *
	 * @since 1.3
	 */
	protected void preInit() {

	}

	/**
	 * Use this method to initialize objects which are needed during rendering.
	 *
	 * @since 1.3
	 */
	protected void postInit() {

	}

	/**
	 * Disposes all resources used by the renderer.
	 * Don't forget to call super.dispose if overwriting this method.
	 *
	 * @since 1.3
	 */
	@Override
	protected void dispose() {
		if (getViewModelContext() != null) {
			getViewModelContext().unregisterViewChangeListener(listener);
		}
		listener = null;
		controls = null;
		getViewModelContext().removeContextUser(this);

		super.dispose();
	}

	/**
	 * Renders the passed {@link VElement}.
	 *
	 * @param cell the {@link SWTGridCell} of the control to render
	 * @param parent the {@link Composite} to render on
	 * @return the rendered {@link Control}
	 * @throws NoRendererFoundException this is thrown when a renderer cannot be found
	 * @throws NoPropertyDescriptorFoundExeption this is thrown when no property descriptor can be found
	 * @since 1.3
	 */
	public Control render(final SWTGridCell cell, Composite parent)
		throws NoRendererFoundException, NoPropertyDescriptorFoundExeption {

		Control control = controls.get(cell);
		if (control != null) {
			return control;
		}

		control = renderControl(cell, parent);
		if (control == null) {
			// something went wrong, log
			return null;
		}
		controls.put(cell, control);

		// register dispose listener to rerender if disposed
		control.addDisposeListener(new DisposeListener() {

			@Override
			public void widgetDisposed(DisposeEvent e) {
				if (controls != null) {
					controls.remove(cell);
				}
			}
		});

		return control;
	}

	/**
	 * Called by the framework to initialize listener.
	 *
	 * @param parent the parent used during render
	 * @since 1.3
	 */
	public void finalizeRendering(Composite parent) {
		if (renderingFinished) {
			return;
		}
		renderingFinished = true;

		// Apply visibility, enablement, and validation decorations
		if (!getVElement().isVisible()) {
			/* convention is to render visible, so only call apply if we are invisible */
			applyVisible();
		}
		applyReadOnly();
		if (!ignoreEnableOnReadOnly()) {
			applyEnable();
		}
		applyValidation();

		// When some control is disposed, dispose me. Don't worry about
		// my parent composite because I could be a detail view that is cached
		// independently of it in a master-detail situation
		if (!controls.isEmpty()) {
			controls.values().iterator().next().addDisposeListener(new DisposeListener() {
@Override public void widgetDisposed(DisposeEvent event) { dispose(); } }); } } /** * Query whether rendering has completed and I am ready for user interaction. * * @return whether rendering has finished * * @since 1.22 */ protected boolean isRenderingFinished() { return renderingFinished; } /** * Returns <code>true</code> when read only will always force control to be disabled. * * @return <code>true</code> when read only will always force control to be disabled. */ protected boolean ignoreEnableOnReadOnly() { return getVElement().isEffectivelyReadonly(); } /** * Renders the passed {@link VElement}. * * @param cell the {@link GridCell} of the control to render * @param parent the {@link Composite} to render on * @return the rendered {@link Control} * @throws NoRendererFoundException this is thrown when a renderer cannot be found * @throws NoPropertyDescriptorFoundExeption this is thrown when no property descriptor can be found * @since 1.3 */ protected abstract Control renderControl(SWTGridCell cell, Composite parent) throws NoRendererFoundException, NoPropertyDescriptorFoundExeption; /** * Marks a controls as readonly. * * @since 1.3 * */ protected void applyReadOnly() { // Do nothing, implement behavior in implementing class if needed } /** * Allows implementers to set a control to enabled. * * @since 1.3 * */ protected void applyEnable() { // Do nothing, implement behavior in implementing class if needed } /** * Wraps the call to enable/disable a control. * * @param gridCell the {@link SWTGridCell} to enable/disable * @param control the {@link Control} to enable/disable * @param enabled true if control should be enabled, false otherwise * @since 1.3 */ protected void setControlEnabled(SWTGridCell gridCell, Control control, boolean enabled) { control.setEnabled(enabled); } /** * Allows implementers to check and set the visibility on the whole result row. * * @since 1.3 * */ protected void applyVisible() { final boolean visible = getVElement().isVisible(); /* avoid multiple layout calls by saving the parents which need to be relayouted */ final Set<Composite> parents = new LinkedHashSet<Composite>(); for (final SWTGridCell gridCell : controls.keySet()) { final Object layoutData = controls.get(gridCell).getLayoutData(); if (GridData.class.isInstance(layoutData)) { final GridData gridData = (GridData) layoutData; if (gridData != null) { gridData.exclude = !visible; } } controls.get(gridCell).setVisible(visible); parents.add(controls.get(gridCell).getParent()); } for (final Composite composite : parents) { EMFFormsSWTLayoutUtil.adjustParentSize(composite.getChildren()[0]); } } /** * Allows implementers to display the validation state of the control. * The default implementation does nothing. * * @since 1.3 */ protected void applyValidation() { } /** * Called before the {@link #applyValidation()}. This method allows to create a diff between the old diagnostic and * the new diagnostic and thus improve the performance of the overlay apply by triggering it only on the relevant * elements. * * @param oldDiagnostic The previous {@link VDiagnostic} * @param newDiagnostic The current {@link VDiagnostic} * @since 1.14 */ protected void applyValidation(VDiagnostic oldDiagnostic, VDiagnostic newDiagnostic) { } /** * @return String the default font name on the system. * @param control The control to derive the default font name from * * @since 1.5 */ protected String getDefaultFontName(Control control) { return control.getDisplay().getSystemFont().getFontData()[0].getName(); } /** * If my control is rendered within a scrolled composite, scroll that composite to reveal me. * * @since 1.22 */ public void scrollToReveal() { if (controls != null && !controls.isEmpty()) { @SuppressWarnings("serial") final Iterator<Control> iter = new AbstractTreeIterator<Control>(controls.values(), false) { @Override protected Iterator<? extends Control> getChildren(Object object) { if (object instanceof Composite) { return Arrays.asList(((Composite) object).getChildren()).iterator(); } if (object instanceof Collection<?>) { @SuppressWarnings("unchecked") final Collection<? extends Control> collection = (Collection<? extends Control>) object; return collection.iterator(); } return Collections.emptyIterator(); } }; while (iter.hasNext()) { final Control next = iter.next(); if (next instanceof Composite) { // Don't attempt to reveal composites continue; } if (scrollToReveal(next)) { break; } } } } /** * Scroll composites as necessary to reveal the given {@code control} and * then request focus. * * @param control the control to reveal and focus * @return whether the focus was successfully set (usually because the control * is one that can receive input focus) * * @since 1.22 */ protected boolean scrollToReveal(final Control control) { for (Composite parent = control.getParent(); parent != null; parent = parent.getParent()) { if (parent instanceof ScrolledComposite) { final ScrolledComposite scrolled = (ScrolledComposite) parent; scrolled.showControl(control); } } // Try to request focus return control.setFocus(); } /** * Query whether a given {@code control} can plausibly be revealed. * * @param control a control to be revealed * @return whether it reasonably can be revealed * * @since 1.22 */ protected boolean canReveal(Control control) { return control != null && !control.isDisposed() && control.isVisible(); } // // Nested types // /** * A listener that reacts to view-model changes to update the UI accordingly. * Examples include updating enablement/visibility state and diagnostic decorations. */ private final class ViewChangeListener implements ModelChangeListener { @Override public void notifyChange(ModelChangeNotification notification) { if (!renderingFinished) { return; } // We ned to handle touch events for the diagnostic if in the case of // re-use of the view model we update diagnostics in situ if (notification.getRawNotification().isTouch() && notification.getStructuralFeature() != VViewPackage.Literals.ELEMENT__DIAGNOSTIC) { return; } // Always apply enable and read-only if it changed because it might have changed for a parent if (notification.getStructuralFeature() == VViewPackage.Literals.ELEMENT__ENABLED) { if (!ignoreEnableOnReadOnly()) { applyEnable(); } } else if (notification.getStructuralFeature() == VViewPackage.Literals.ELEMENT__READONLY) { applyReadOnly(); } if (notification.getNotifier() != getVElement()) { return; } if (notification.getStructuralFeature() == VViewPackage.Literals.ELEMENT__VISIBLE) { applyVisible(); } else if (notification.getStructuralFeature() == VViewPackage.Literals.ELEMENT__DIAGNOSTIC) { final VDiagnostic newDia = (VDiagnostic) notification.getRawNotification().getNewValue(); final VDiagnostic oldDia = (VDiagnostic) notification.getRawNotification().getOldValue(); applyValidation(oldDia, newDia); applyValidation(); } } } }