Skip to content

Content of file TreeMasterDetailSWTRenderer.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:
 * Anas Chakfeh - initial API and implementation
 * Eugen Neufeld - Refactoring
 * Alexandra Buzila - Refactoring
 * Johannes Faltermeier - integration with validation service
 * Christian W. Damus - bugs 543376, 545460, 527686, 548592, 552385
 ******************************************************************************/
package org.eclipse.emf.ecp.view.spi.treemasterdetail.ui.swt;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.inject.Inject;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CompoundCommand;
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.impl.DynamicEObjectImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecp.common.spi.ChildrenDescriptorCollector;
import org.eclipse.emf.ecp.edit.internal.swt.util.OverlayImageDescriptor;
import org.eclipse.emf.ecp.edit.spi.ConditionalDeleteService;
import org.eclipse.emf.ecp.edit.spi.DeleteService;
import org.eclipse.emf.ecp.edit.spi.EMFDeleteServiceImpl;
import org.eclipse.emf.ecp.edit.spi.ReferenceService;
import org.eclipse.emf.ecp.ui.view.swt.ECPSWTViewRenderer;
import org.eclipse.emf.ecp.view.internal.swt.ContextMenuViewModelService;
import org.eclipse.emf.ecp.view.internal.treemasterdetail.ui.swt.Activator;
import org.eclipse.emf.ecp.view.model.common.edit.provider.CustomReflectiveItemProviderAdapterFactory;
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.ModelChangeAddRemoveListener;
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.VView;
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.masterdetail.DetailViewCache;
import org.eclipse.emf.ecp.view.spi.swt.masterdetail.DetailViewManager;
import org.eclipse.emf.ecp.view.spi.swt.selection.IMasterDetailSelectionProvider;
import org.eclipse.emf.ecp.view.spi.swt.services.ECPSelectionProviderService;
import org.eclipse.emf.ecp.view.treemasterdetail.model.VTreeMasterDetail;
import org.eclipse.emf.ecp.view.treemasterdetail.ui.swt.internal.RootObject;
import org.eclipse.emf.ecp.view.treemasterdetail.ui.swt.internal.TreeMasterDetailSelectionManipulatorHelper;
import org.eclipse.emf.edit.command.AddCommand;
import org.eclipse.emf.edit.command.CommandParameter;
import org.eclipse.emf.edit.command.DeleteCommand;
import org.eclipse.emf.edit.command.SetCommand;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.provider.ComposedAdapterFactory;
import org.eclipse.emf.edit.ui.action.ecp.CreateChildAction;
import org.eclipse.emf.edit.ui.dnd.EditingDomainViewerDropAdapter;
import org.eclipse.emf.edit.ui.dnd.LocalTransfer;
import org.eclipse.emf.edit.ui.dnd.ViewerDragAdapter;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryLabelProvider;
import org.eclipse.emfforms.common.Optional;
import org.eclipse.emfforms.spi.common.report.ReportService;
import org.eclipse.emfforms.spi.swt.core.AbstractSWTRenderer;
import org.eclipse.emfforms.spi.swt.core.layout.GridDescriptionFactory;
import org.eclipse.emfforms.spi.swt.core.layout.SWTGridCell;
import org.eclipse.emfforms.spi.swt.core.layout.SWTGridDescription;
import org.eclipse.emfforms.spi.swt.core.ui.SWTValidationHelper;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.ToolBar;
import org.osgi.framework.FrameworkUtil;

/**
 * SWT Renderer for a {@link VTreeMasterDetail} element.
 *
 * @author Anas Chakfeh
 * @author Eugen Neufeld
 * @since 1.5
 *
 */
@SuppressWarnings("deprecation")
public class TreeMasterDetailSWTRenderer extends AbstractSWTRenderer<VTreeMasterDetail> {

	private static final String ENABLE_MULTI_EDIT = "enableMultiEdit"; //$NON-NLS-1$

	/**
	 * 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
	 */
	@Inject
	public TreeMasterDetailSWTRenderer(final VTreeMasterDetail vElement, final ViewModelContext viewContext,
		ReportService reportService) {

		super(vElement, viewContext, reportService);
	}

	/**
	 * The detail key passed to the view model context.
	 */
	public static final String DETAIL_KEY = DetailViewManager.DETAIL_PROPERTY;

	/**
	 * Menu separator ID for the group to which additional menu contributions are added in the
	 * tree's context menu.
	 */
	public static final String GLOBAL_ADDITIONS = "global_additions"; //$NON-NLS-1$

	/**
	 * Context key for the root.
	 */
	public static final String ROOT_KEY = "root"; //$NON-NLS-1$

	private SWTGridDescription rendererGridDescription;

	private Font detailsFont;
	private Color titleColor;
	private Font titleFont;
	private Color headerBgColor;
	private TreeViewer treeViewer;

	private ScrolledComposite rightPanel;

	private Composite container;

	private Composite rightPanelContainerComposite;

	private ModelChangeListener domainModelListener;
	private ViewModelContext childContext;
	private DetailViewManager detailManager;

	/**
	 * @author jfaltermeier
	 *
	 */
	private final class MasterTreeContextMenuListener implements IMenuListener {
		private final EditingDomain editingDomain;
		private final TreeViewer treeViewer;
		private final ChildrenDescriptorCollector childrenDescriptorCollector;
		private final List<MasterDetailAction> menuActions;

		/**
		 * @param editingDomain
		 * @param treeViewer
		 * @param childrenDescriptorCollector
		 * @param menuActions
		 */
		private MasterTreeContextMenuListener(EditingDomain editingDomain, TreeViewer treeViewer,
			ChildrenDescriptorCollector childrenDescriptorCollector, List<MasterDetailAction> menuActions) {
			this.editingDomain = editingDomain;
			this.treeViewer = treeViewer;
			this.childrenDescriptorCollector = childrenDescriptorCollector;
			this.menuActions = menuActions;
		}

		@Override
		public void menuAboutToShow(IMenuManager manager) {
			if (getVElement().isEffectivelyReadonly() || !getVElement().isEffectivelyEnabled()) {
				return;
			}
			if (treeViewer.getSelection().isEmpty()) {
				fillMenu(null, manager);
				return;
			}
			final EObject root = ((RootObject) treeViewer.getInput()).getRoot();

			if (treeViewer.getSelection() instanceof IStructuredSelection) {
				final IStructuredSelection selection = (IStructuredSelection) treeViewer.getSelection();

				if (selection.size() == 1 && EObject.class.isInstance(selection.getFirstElement())) {
					final EObject eObject = (EObject) selection.getFirstElement();
					final EditingDomain domain = AdapterFactoryEditingDomain.getEditingDomainFor(eObject);
					if (domain == null) {
						return;
					}
					final Collection<?> descriptors = childrenDescriptorCollector.getDescriptors(eObject);
					fillContextMenu(manager, descriptors, editingDomain, eObject);
				}
				if (!selection.toList().contains(root)) {
					manager.add(new Separator(GLOBAL_ADDITIONS));
					addDeleteActionToContextMenu(editingDomain, manager, selection);
				}
				manager.add(new Separator());

				if (selection.getFirstElement() != null && EObject.class.isInstance(selection.getFirstElement())) {
					final EObject selectedObject = (EObject) selection.getFirstElement();

					fillMenu(selectedObject, manager);
				}
			}
		}

		private void fillMenu(final EObject selectedObject, IMenuManager manager) {
			for (final MasterDetailAction menuAction : menuActions) {
				if (menuAction.shouldShow(selectedObject)) {
					final Action newAction = new Action() {
						@Override
						public void run() {
							super.run();
							menuAction.execute(selectedObject);
						}
					};

					newAction.setImageDescriptor(ImageDescriptor.createFromURL(FrameworkUtil.getBundle(
						menuAction.getClass())
						.getResource(menuAction.getImagePath())));
					newAction.setText(menuAction.getLabel());

					manager.add(newAction);
				}
			}
		}
	}

	@Override
	protected void dispose() {
		rendererGridDescription = null;
		if (detailManager != null) {
			detailManager.dispose();
		}
		childContext = null;
		if (getViewModelContext() != null && domainModelListener != null) {
			getViewModelContext().unregisterDomainChangeListener(domainModelListener);
		}
		domainModelListener = null;
		if (treeViewer != null) {
			treeViewer.setInput(null);
		}
		super.dispose();
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see org.eclipse.emfforms.spi.swt.core.AbstractSWTRenderer#getGridDescription(SWTGridDescription)
	 */
	@Override
	public SWTGridDescription getGridDescription(SWTGridDescription gridDescription) {
		if (rendererGridDescription == null) {
			rendererGridDescription = GridDescriptionFactory.INSTANCE.createSimpleGrid(1, 1, this);
		}
		return rendererGridDescription;
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see org.eclipse.emfforms.spi.swt.core.AbstractSWTRenderer#renderControl(org.eclipse.emfforms.spi.swt.core.layout.SWTGridCell,
	 *      org.eclipse.swt.widgets.Composite)
	 */
	@Override
	protected Control renderControl(SWTGridCell cell, Composite parent) throws NoRendererFoundException,
		NoPropertyDescriptorFoundExeption {

		/* The tree's composites */
		final Composite form = createMasterDetailForm(parent);

		createHeader(form);

		final SashForm sash = createSash(form);

		final Composite masterPanel = createMasterPanel(sash);

		createRightPanelContent(sash);

		sash.setWeights(new int[] { 1, 3 });

		createMasterTree(masterPanel);

		if (hasContextMenu()) {
			registerControlAsContextMenuReceiver();
		}
		form.layout(true);
		return form;
	}

	private void registerControlAsContextMenuReceiver() {
		if (!getViewModelContext().hasService(ContextMenuViewModelService.class)) {
			return;
		}
		final ContextMenuViewModelService service = getViewModelContext().getService(
			ContextMenuViewModelService.class);

		if (service != null) {
			service.setParentControl(treeViewer.getTree());
			service.registerContextMenu();
		}
	}

	/**
	 * Creates the sashform for the master detail colums.
	 *
	 * @param parent the parent
	 * @return the sash
	 */
	protected SashForm createSash(Composite parent) {
		/* THe contents of the composite */
		final Composite sashComposite = new Composite(parent, SWT.FILL);
		final GridLayout sashLayout = GridLayoutFactory.fillDefaults().create();
		sashLayout.marginWidth = 5;
		sashComposite.setLayout(sashLayout);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(sashComposite);

		final SashForm sash = new SashForm(sashComposite, SWT.HORIZONTAL);

		sash.setBackground(parent.getBackground());
		GridLayoutFactory.fillDefaults().numColumns(2).equalWidth(false).applyTo(sash);
		GridDataFactory.fillDefaults().grab(true, true).align(SWT.FILL, SWT.FILL).applyTo(sash);
		sash.setSashWidth(5);
		return sash;
	}

	/**
	 * Create the parent of the master detail form.
	 *
	 * @param parent the parent
	 * @return the composite
	 */
	protected Composite createMasterDetailForm(Composite parent) {
		final Composite form = new Composite(parent, SWT.BORDER);
		final GridLayout layout = GridLayoutFactory.fillDefaults().create();

		form.setLayout(layout);
		form.setBackgroundMode(SWT.INHERIT_FORCE);
		// form.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_WHITE));
		return form;
	}

	/**
	 * Creates the tree viewer for the master.
	 *
	 * @param masterPanel the parent
	 * @return the tree viewer
	 */
	protected TreeViewer createMasterTree(final Composite masterPanel) {
		final EObject modelElement = getViewModelContext().getDomainModel();
		final EditingDomain editingDomain = AdapterFactoryEditingDomain.getEditingDomainFor(modelElement);

		final ComposedAdapterFactory adapterFactory = new ComposedAdapterFactory(new AdapterFactory[] {
			new CustomReflectiveItemProviderAdapterFactory(),
			new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE) });

		final AdapterFactoryContentProvider adapterFactoryContentProvider = new AdapterFactoryContentProvider(
			adapterFactory) {

			@Override
			public Object[] getElements(Object object) {
				return new Object[] { ((RootObject) object).getRoot() };
			}
		};
		final AdapterFactoryLabelProvider labelProvider = new TreeMasterDetailLabelProvider(adapterFactory);

		treeViewer = new TreeViewer(masterPanel);

		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).hint(100, SWT.DEFAULT)
			.applyTo(treeViewer.getTree());

		treeViewer.setContentProvider(adapterFactoryContentProvider);
		treeViewer.setLabelProvider(getLabelProvider(labelProvider));
		treeViewer.setAutoExpandLevel(2); // top level element is expanded, but not the children
		treeViewer.setInput(new RootObject(modelElement));

		domainModelListener = new ModelChangeAddRemoveListener() {

			@Override
			public void notifyChange(ModelChangeNotification notification) {
				// nothing to do here
			}

			// workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=27480
			// the treeviewer doesn't autoexpand on refresh
			@Override
			public void notifyAdd(Notifier notifier) {
				if (isRenderingFinished()) {
					treeViewer.expandToLevel(notifier, 1);
				}
			}

			@Override
			public void notifyRemove(Notifier notifier) {
				// If an element is deleted, reset the selection to the root node
				if (childContext != null && notifier == childContext.getDomainModel()) {
					treeViewer.setSelection(new StructuredSelection(getViewModelContext().getDomainModel()));
				}
			}
		};
		getViewModelContext().registerDomainChangeListener(domainModelListener);

		// Drag and Drop
		if (hasDnDSupport()) {
			addDragAndDropSupport(modelElement, treeViewer, editingDomain);
		}

		// Selection Listener
		final TreeMasterViewSelectionListener treeMasterViewSelectionListener = new TreeMasterViewSelectionListener();
		treeViewer.addSelectionChangedListener(treeMasterViewSelectionListener);
		treeViewer.setSelection(new StructuredSelection(modelElement));
		if (hasContextMenu()) {
			fillContextMenu(treeViewer, editingDomain);
		}

		treeViewer.getTree().addDisposeListener(new DisposeListener() {

			@Override
			public void widgetDisposed(DisposeEvent event) {
				adapterFactoryContentProvider.dispose();
				labelProvider.dispose();
				adapterFactory.dispose();
				if (titleFont != null) {
					titleFont.dispose();
				}
				if (detailsFont != null) {
					detailsFont.dispose();
				}
				if (titleColor != null) {
					titleColor.dispose();
				}
				if (headerBgColor != null) {
					headerBgColor.dispose();
				}
				treeViewer.removeSelectionChangedListener(treeMasterViewSelectionListener);
			}
		});

		// Register my tree viewer as the selection provider for my element
		final ECPSelectionProviderService sps = getViewModelContext().getService(ECPSelectionProviderService.class);
		final IMasterDetailSelectionProvider mdSelectionProvider = sps.createMasterDetailSelectionProvider(treeViewer,
			() -> rightPanel);
		sps.registerSelectionProvider(getVElement(), mdSelectionProvider);

		return treeViewer;
	}

	/**
	 * Return true if a context menu should be shown in the tree.
	 *
	 * @return true if a context menu should be shown, false otherwise
	 */
	protected boolean hasContextMenu() {
		return !getVElement().isEffectivelyReadonly();
	}

	/**
	 * Return true if the tree should support DnD.
	 *
	 * @return true if DnD should be supported , false otherwise
	 */
	protected boolean hasDnDSupport() {
		return !getVElement().isEffectivelyReadonly();
	}

	/**
	 * Returns the label provider.
	 *
	 * @param adapterFactoryLabelProvider the adaper factory label provider
	 * @return the label provider to use for the tree
	 */
	protected ILabelProvider getLabelProvider(final AdapterFactoryLabelProvider adapterFactoryLabelProvider) {
		return adapterFactoryLabelProvider;
	}

	/**
	 * Creates the composite for the master panel.
	 *
	 * @param sash the parent
	 * @return the composite
	 */
	protected Composite createMasterPanel(final SashForm sash) {
		final Composite leftPanel = new Composite(sash, SWT.NONE);
		leftPanel.setLayout(GridLayoutFactory.fillDefaults().create());
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(leftPanel);
		// leftPanel.setBackground(sash.getBackground());
		leftPanel.setBackgroundMode(SWT.INHERIT_FORCE);
		return leftPanel;
	}

	/**
	 * Adds the header to a parent composite.
	 *
	 * @param parent the parent
	 */
	protected void createHeader(Composite parent) {
		final Composite headerComposite = new Composite(parent, SWT.NONE);
		final GridLayout headerLayout = GridLayoutFactory.fillDefaults().create();
		headerComposite.setLayout(headerLayout);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(headerComposite);
		headerBgColor = new Color(parent.getDisplay(), new RGB(220, 240, 247));
		headerComposite.setBackground(headerBgColor);

		final EObject modelElement = getViewModelContext().getDomainModel();
		final EditingDomain editingDomain = AdapterFactoryEditingDomain.getEditingDomainFor(modelElement);

		/* The header of the composite */
		if (modelElement.eContainer() == null && !DynamicEObjectImpl.class.isInstance(modelElement)) {

			final Composite header = getPageHeader(headerComposite);
			final List<Action> actions = readToolbarActions(modelElement, editingDomain);

			final ToolBar toolBar = new ToolBar(header, SWT.FLAT | SWT.RIGHT);
			final FormData formData = new FormData();
			formData.right = new FormAttachment(100, 0);
			toolBar.setLayoutData(formData);
			toolBar.layout();
			final ToolBarManager toolBarManager = new ToolBarManager(toolBar);

			/* Add actions to header */
			for (final Action action : actions) {
				toolBarManager.add(action);
			}
			toolBarManager.update(true);
			header.layout();

		}
	}

	private Composite getPageHeader(Composite parent) {
		final Composite header = new Composite(parent, SWT.FILL);
		final FormLayout layout = new FormLayout();
		layout.marginHeight = 5;
		layout.marginWidth = 5;
		header.setLayout(layout);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(header);

		header.setBackground(parent.getBackground());

		final Label titleImage = new Label(header, SWT.FILL);
		final ImageDescriptor imageDescriptor = ImageDescriptor.createFromURL(Activator.getDefault()
			.getBundle()
			.getResource("icons/view.png")); //$NON-NLS-1$
		titleImage.setImage(new Image(parent.getDisplay(), imageDescriptor.getImageData()));
		final FormData titleImageData = new FormData();
		final int imageOffset = -titleImage.computeSize(SWT.DEFAULT, SWT.DEFAULT).y / 2;
		titleImageData.top = new FormAttachment(50, imageOffset);
		titleImageData.left = new FormAttachment(0, 10);
		titleImage.setLayoutData(titleImageData);

		final Label title = new Label(header, SWT.WRAP);
		title.setText("View Editor"); //$NON-NLS-1$
		titleFont = new Font(title.getDisplay(), getDefaultFontName(title), 12, SWT.BOLD);
		title.setFont(titleFont);
		title.setForeground(getTitleColor(parent));
		final int titleHeight = title.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
		final FormData titleData = new FormData();
		titleData.left = new FormAttachment(titleImage, 5, SWT.DEFAULT);
		titleData.top = new FormAttachment(50, -titleHeight / 2);
		title.setLayoutData(titleData);

		return header;

	}

	private Color getTitleColor(Composite parent) {
		if (titleColor == null) {
			titleColor = new Color(parent.getDisplay(), new RGB(25, 76, 127));
		}
		return titleColor;
	}

	/**
	 * Creates the composite holding the details.
	 *
	 * @param parent the parent
	 * @return the right panel/detail composite
	 */
	protected ScrolledComposite createRightPanelContent(Composite parent) {
		rightPanel = new ScrolledComposite(parent, SWT.V_SCROLL | SWT.H_SCROLL);
		rightPanel.setShowFocusedControl(true);
		rightPanel.setExpandVertical(true);
		rightPanel.setExpandHorizontal(true);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(rightPanel);
		rightPanel.setLayout(GridLayoutFactory.fillDefaults().create());
		rightPanel.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_WHITE));

		container = new Composite(rightPanel, SWT.FILL);
		container.setLayout(GridLayoutFactory.fillDefaults().create());
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(container);
		container.setBackground(rightPanel.getBackground());

		/* The header */
		final Composite header = new Composite(container, SWT.FILL);
		final GridLayout headerLayout = GridLayoutFactory.fillDefaults().create();
		header.setLayout(headerLayout);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(header);
		header.setBackground(rightPanel.getBackground());

		final Label label = new Label(header, SWT.WRAP);
		label.setText("Details"); //$NON-NLS-1$
		detailsFont = new Font(label.getDisplay(), getDefaultFontName(label), 10, SWT.BOLD);
		label.setFont(detailsFont);
		label.setForeground(getTitleColor(parent));
		label.setBackground(header.getBackground());

		rightPanelContainerComposite = new Composite(container, SWT.FILL);
		rightPanelContainerComposite.setLayout(GridLayoutFactory.fillDefaults().create());
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true)
			.applyTo(rightPanelContainerComposite);
		rightPanelContainerComposite.setBackground(rightPanel.getBackground());

		rightPanel.setContent(container);

		rightPanel.layout();
		container.layout();

		final Point point = container.computeSize(SWT.DEFAULT, SWT.DEFAULT);
		rightPanel.setMinSize(point);

		detailManager = new DetailViewManager(rightPanelContainerComposite);
		detailManager.setCache(DetailViewCache.createCache(getViewModelContext()));
		detailManager.layoutDetailParent(rightPanelContainerComposite);

		return rightPanel;
	}

	@Override
	protected String getDefaultFontName(Control control) {
		return control.getDisplay().getSystemFont().getFontData()[0].getName();
	}

	private List<Action> readToolbarActions(EObject modelElement, final EditingDomain editingDomain) {
		final List<Action> actions = new ArrayList<Action>();
		final IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry();
		if (extensionRegistry == null) {
			return actions;
		}
		if (!VView.class.isInstance(modelElement)) {
			return actions;
		}
		final VView view = (VView) modelElement;

		final IConfigurationElement[] controls = extensionRegistry
			.getConfigurationElementsFor("org.eclipse.emf.ecp.view.treemasterdetail.ui.swt.masterDetailActions"); //$NON-NLS-1$
		for (final IConfigurationElement e : controls) {
			try {
				final String location = e.getAttribute("location"); //$NON-NLS-1$
				if (!location.equals("toolbar")) { //$NON-NLS-1$
					continue;
				}

				final String label = e.getAttribute("label"); //$NON-NLS-1$
				final String imagePath = e.getAttribute("imagePath"); //$NON-NLS-1$
				final MasterDetailAction command = (MasterDetailAction) e.createExecutableExtension("command"); //$NON-NLS-1$
				final Action newAction = new Action() {
					@Override
					public void run() {
						super.run();
						command.execute(view);
					}
				};

				newAction.setImageDescriptor(ImageDescriptor.createFromURL(FrameworkUtil.getBundle(command.getClass())
					.getResource(imagePath)));
				newAction.setText(label);
				actions.add(newAction);
			} catch (final CoreException e1) {
				e1.printStackTrace();
			}
		}
		return actions;
	}

	private void addDragAndDropSupport(final EObject modelElement, final TreeViewer treeViewer,
		EditingDomain editingDomain) {

		final int dndOperations = DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_LINK;
		final Transfer[] transfers = new Transfer[] { LocalTransfer.getInstance() };
		treeViewer.addDragSupport(dndOperations, transfers, new ViewerDragAdapter(treeViewer));
		final EditingDomainViewerDropAdapter editingDomainViewerDropAdapter = new EditingDomainViewerDropAdapter(
			editingDomain,
			treeViewer);
		treeViewer.addDropSupport(dndOperations, transfers, editingDomainViewerDropAdapter);
	}

	/**
	 * @param treeViewer
	 * @param editingDomain
	 */
	private void fillContextMenu(final TreeViewer treeViewer, final EditingDomain editingDomain) {
		final ChildrenDescriptorCollector childrenDescriptorCollector = new ChildrenDescriptorCollector();
		final List<MasterDetailAction> menuActions = readMasterDetailActions();
		final MenuManager menuMgr = new MenuManager();
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(new MasterTreeContextMenuListener(editingDomain, treeViewer,
			childrenDescriptorCollector, menuActions));
		final Menu menu = menuMgr.createContextMenu(treeViewer.getControl());
		treeViewer.getControl().setMenu(menu);
	}

	/**
	 * Returns a list of all {@link MasterDetailAction MasterDetailActions} which shall be displayed in the context menu
	 * of the master treeviewer.
	 *
	 * @return the actions
	 */
	protected List<MasterDetailAction> readMasterDetailActions() {
		final List<MasterDetailAction> commands = new ArrayList<MasterDetailAction>();
		final IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry();
		if (extensionRegistry == null) {
			return commands;
		}

		final IConfigurationElement[] controls = extensionRegistry
			.getConfigurationElementsFor("org.eclipse.emf.ecp.view.treemasterdetail.ui.swt.masterDetailActions"); //$NON-NLS-1$
		for (final IConfigurationElement e : controls) {
			try {
				final String location = e.getAttribute("location"); //$NON-NLS-1$
				if (!location.equals("menu")) { //$NON-NLS-1$
					continue;
				}
				final String label = e.getAttribute("label"); //$NON-NLS-1$
				final String imagePath = e.getAttribute("imagePath"); //$NON-NLS-1$
				final MasterDetailAction command = (MasterDetailAction) e.createExecutableExtension("command"); //$NON-NLS-1$
				command.setLabel(label);
				command.setImagePath(imagePath);
				command.setTreeViewer(treeViewer);

				commands.add(command);

			} catch (final CoreException ex) {
				Activator.getDefault().getLog().log(
					new Status(IStatus.ERROR, Activator.getDefault().getBundle().getSymbolicName(),
						ex.getMessage(), ex));
			}
		}

		return commands;

	}

	/**
	 * @param manager The menu manager responsible for the context menu
	 * @param descriptors The menu items to be added
	 * @param domain The editing domain of the current EObject
	 * @param eObject The model element
	 */
	private void fillContextMenu(IMenuManager manager, Collection<?> descriptors, final EditingDomain domain,
		final EObject eObject) {
		for (final Object descriptor : descriptors) {

			final CommandParameter cp = (CommandParameter) descriptor;
			if (!CommandParameter.class.isInstance(descriptor)) {
				continue;
			}
			if (cp.getEReference() == null) {
				continue;
			}
			if (!cp.getEReference().isMany() && eObject.eIsSet(cp.getEStructuralFeature())) {
				continue;
			} else if (cp.getEReference().isMany() && cp.getEReference().getUpperBound() != -1
				&& cp.getEReference().getUpperBound() <= ((List<?>) eObject.eGet(cp.getEReference())).size()) {
				continue;
			}

			manager.add(new CreateChildAction(domain, new StructuredSelection(eObject), descriptor) {
				@Override
				public void run() {
					super.run();

					final EReference reference = ((CommandParameter) descriptor).getEReference();
					// if (!reference.isContainment()) {
					// domain.getCommandStack().execute(
					// AddCommand.create(domain, eObject.eContainer(), null, cp.getEValue()));
					// }

					domain.getCommandStack().execute(
						AddCommand.create(domain, eObject, reference, cp.getEValue()));
				}
			});
		}

	}

	/**
	 * @param editingDomain
	 * @param manager
	 * @param selection
	 */
	private void addDeleteActionToContextMenu(final EditingDomain editingDomain, final IMenuManager manager,
		final IStructuredSelection selection) {

		final Action deleteAction = new Action() {
			@SuppressWarnings("unchecked")
			@Override
			public void run() {
				super.run();
				DeleteService deleteService = getViewModelContext().getService(DeleteService.class);
				if (deleteService == null) {
					/*
					 * #getService(Class<?>) will report to the reportservice if it could not be found
					 * Use Default
					 */
					deleteService = new EMFDeleteServiceImpl();
				}
				deleteService.deleteElements(selection.toList());
			}
		};
		final String deleteImagePath = "icons/delete.png";//$NON-NLS-1$
		deleteAction.setImageDescriptor(ImageDescriptor.createFromURL(Activator.getDefault()
			.getBundle()
			.getResource(deleteImagePath)));
		deleteAction.setText("Delete"); //$NON-NLS-1$
		deleteAction.setEnabled(ConditionalDeleteService.getDeleteService(getViewModelContext()).canDelete(selection.toList()));
		manager.add(deleteAction);
	}

	/**
	 * Allows to manipulate the view context for the selected element that is about to be rendered.
	 *
	 * @param viewContext the view context.
	 */
	protected void manipulateViewContext(ViewModelContext viewContext) {
		// do nothing
	}

	/**
	 *
	 * @author Anas Chakfeh
	 *         This class is responsible for handling selection changed events which happen on the tree
	 *
	 */
	private class TreeMasterViewSelectionListener implements ISelectionChangedListener {

		/**
		 * Adapter which listens to changes and delegates the notification to other EObjects.
		 *
		 * @author Eugen Neufeld
		 *
		 */
		private final class MultiEditAdapter extends AdapterImpl {
			private final EObject dummy;
			private final Set<EObject> selectedEObjects;

			private MultiEditAdapter(EObject dummy, Set<EObject> selectedEObjects) {
				this.dummy = dummy;
				this.selectedEObjects = selectedEObjects;
			}

			@Override
			public void notifyChanged(Notification notification) {
				final EditingDomain editingDomain = AdapterFactoryEditingDomain
					.getEditingDomainFor(getViewModelContext().getDomainModel());
				if (dummy.eClass().getEAllAttributes().contains(notification.getFeature())) {
					final CompoundCommand cc = new CompoundCommand();
					for (final EObject selected : selectedEObjects) {
						Command command = null;
						switch (notification.getEventType()) {
						case Notification.SET:
							command = SetCommand.create(editingDomain, selected,
								notification.getFeature(), notification.getNewValue());
							break;
						case Notification.UNSET:
							command = SetCommand.create(editingDomain, selected,
								notification.getFeature(), SetCommand.UNSET_VALUE);
							break;
						case Notification.ADD:
						case Notification.ADD_MANY:
							command = AddCommand.create(editingDomain, selected,
								notification.getFeature(), notification.getNewValue());
							break;
						case Notification.REMOVE:
						case Notification.REMOVE_MANY:
							command = DeleteCommand.create(editingDomain, notification.getOldValue());
							break;
						default:
							continue;
						}
						cc.append(command);
					}
					editingDomain.getCommandStack().execute(cc);
				}
			}
		}

		@Override
		public void selectionChanged(SelectionChangedEvent event) {
			final IStructuredSelection selection = (IStructuredSelection) event.getSelection();
			final Object treeSelected = getSelection(selection);

			detailManager.cacheCurrentDetail();
			cleanCustomOnSelectionChange();

			final Object selectedObject = treeSelected == null ? treeSelected : manipulateSelection(treeSelected);
			if (selectedObject instanceof EObject) {
				final EObject selected = (EObject) selectedObject;
				final Object root = manipulateSelection(((RootObject) ((TreeViewer) event.getSource()).getInput())
					.getRoot());

				final boolean rootSelected = selected == root;
				VView view = null;
				if (rootSelected) {
					view = getVElement().getDetailView();
				}
				if (view == null || view.getChildren().isEmpty()) {
					view = detailManager.getDetailView(getViewModelContext(), selected,
						properties -> {
							if (rootSelected) {
								properties.addNonInheritableProperty(ROOT_KEY, true);
							}
						});
				}

				if (detailManager.isCached(selected)) {
					detailManager.activate(selected);
				} else {
					final ReferenceService referenceService = getViewModelContext().getService(
						ReferenceService.class);
					// we have a multi selection, the multi edit is enabled and the multi selection is valid
					if (getViewModelContext().getContextValue(ENABLE_MULTI_EDIT) == Boolean.TRUE
						&& selection.size() > 1
						&& selected != getSelection(new StructuredSelection(selection.getFirstElement()))) {
						childContext = ViewModelContextFactory.INSTANCE.createViewModelContext(view, selected,
							new TreeMasterDetailReferenceService(referenceService));
					} else {
						childContext = getViewModelContext().getChildContext(selected, getVElement(), view,
							new TreeMasterDetailReferenceService(referenceService));
					}

					manipulateViewContext(childContext);

					detailManager.render(childContext, ECPSWTViewRenderer.INSTANCE::render);
				}

				detailManager.setDetailReadOnly(!getVElement().isEffectivelyEnabled()
					|| getVElement().isEffectivelyReadonly());
			} else {
				// No selection
				childContext = null;
			}

			relayoutDetail();
		}

		private Object getSelection(IStructuredSelection selection) {
			Object treeSelected = selection != null ? selection.getFirstElement() : null;
			if (getViewModelContext().getContextValue(ENABLE_MULTI_EDIT) == Boolean.TRUE
				&& treeSelected instanceof EObject && selection.size() > 1) {
				boolean allOfSameType = true;
				final EObject dummy = EcoreUtil.create(((EObject) treeSelected).eClass());

				final Iterator<?> iterator = selection.iterator();
				final Set<EObject> selectedEObjects = new LinkedHashSet<EObject>();
				while (iterator.hasNext()) {
					final EObject eObject = (EObject) iterator.next();
					allOfSameType &= eObject.eClass() == dummy.eClass();
					if (allOfSameType) {
						for (final EAttribute attribute : dummy.eClass().getEAllAttributes()) {
							if (eObject == treeSelected) {
								dummy.eSet(attribute, eObject.eGet(attribute));
							} else if (dummy.eGet(attribute) != null
								&& !dummy.eGet(attribute).equals(eObject.eGet(attribute))) {
								dummy.eUnset(attribute);
							}
						}
						selectedEObjects.add(eObject);
					} else {
						break;
					}
				}
				if (allOfSameType) {
					treeSelected = dummy;
					dummy.eAdapters().add(new MultiEditAdapter(dummy, selectedEObjects));
				}
			}
			return treeSelected;
		}
	}

	/**
	 * Returns the composite for the detail.
	 *
	 * @return the composite
	 */
	protected Composite getDetailContainer() {
		return rightPanelContainerComposite;
	}

	/**
	 * Allows to manipulate the selection by returning a specific child.
	 *
	 * @param treeSelected the selected element in the tree
	 * @return the object that should be used as a selection
	 */
	protected Object manipulateSelection(Object treeSelected) {
		return TreeMasterDetailSelectionManipulatorHelper.manipulateSelection(treeSelected);
	}

	/**
	 * Gets called after a detail composite was disposed. Allows for further cleanup.
	 */
	protected void cleanCustomOnSelectionChange() {
		// do nothing
	}

	/**
	 * Relayouts the detail composite.
	 */
	protected void relayoutDetail() {
		rightPanelContainerComposite.layout();
		final Point point = container.computeSize(SWT.DEFAULT, SWT.DEFAULT);
		rightPanel.setMinSize(point);
	}

	/**
	 * Reveal the given {@code object} in my tree.
	 *
	 * @param object an object to reveal
	 * @return whether I succeeded in revealing it
	 *
	 * @since 1.22
	 */
	public boolean reveal(Object object) {
		final TreePath treePath = getTreePathFor(object);
		return reveal(treePath);
	}

	/**
	 * Get a path to an {@object} in my tree.
	 *
	 * @param object an object in my tree
	 * @return a path to it
	 *
	 * @since 1.22
	 */
	public TreePath getTreePathFor(Object object) {
		final ITreeContentProvider content = (ITreeContentProvider) treeViewer.getContentProvider();
		final Collection<?> roots = Arrays.asList(content.getElements(treeViewer.getInput()));

		final List<Object> path = new LinkedList<Object>();
		path.add(object);
		for (Object parent = content.getParent(object); parent != null; parent = content.getParent(parent)) {
			path.add(0, parent);

			// Don't go above the root element
			if (roots.contains(parent)) {
				break;
			}
		}

		return new TreePath(path.toArray());
	}

	/**
	 * Reveal the given {@code path} in my tree.
	 *
	 * @param path a tree path to reveal
	 * @return whether I succeeded in revealing it
	 *
	 * @since 1.22
	 */
	public boolean reveal(TreePath path) {
		final ISelection newSelection = new TreeSelection(path);
		if (!newSelection.equals(treeViewer.getSelection())) {
			treeViewer.setSelection(new TreeSelection(path), true);
		}
		treeViewer.reveal(path);

		return treeViewer.getStructuredSelection().getFirstElement() == path.getLastSegment();
	}

	/**
	 * Query whether the given {@code path} exists in my tree.
	 *
	 * @param path a tree path
	 * @return whether the path locates an element that exists in my tree
	 *
	 * @since 1.22
	 */
	public boolean hasPath(TreePath path) {
		if (path.equals(TreePath.EMPTY)) {
			return true;
		}

		final TreePath parentPath = path.getParentPath();
		if (!hasPath(parentPath)) {
			return false;
		}

		final ITreeContentProvider content = (ITreeContentProvider) treeViewer.getContentProvider();
		Collection<?> children;
		if (parentPath.equals(TreePath.EMPTY)) {
			children = Arrays.asList(content.getElements(treeViewer.getInput()));
		} else if (content.hasChildren(parentPath.getLastSegment())) {
			children = Arrays.asList(content.getChildren(parentPath.getLastSegment()));
		} else {
			children = Collections.EMPTY_SET;
		}

		return children.contains(path.getLastSegment());
	}

	/**
	 * Obtain the current detail context, if any.
	 *
	 * @return the view-model context of the details currently being presented,
	 *         or {@code null} if none (usually because there is no selection in the tree)
	 *
	 * @since 1.22
	 */
	public ViewModelContext getDetailContext() {
		return childContext;
	}

	/**
	 * The label provider used for the detail tree.
	 *
	 * @author jfaltermeier
	 *
	 */
	private class TreeMasterDetailLabelProvider extends AdapterFactoryLabelProvider {
TreeMasterDetailLabelProvider(AdapterFactory adapterFactory) { super(adapterFactory); } @Override public Image getImage(Object object) { final Image image = super.getImage(object); if (!EObject.class.isInstance(object)) { return image; } return getValidationOverlay(image, (EObject) object); } protected Image getValidationOverlay(Image image, final EObject object) { // final Integer severity = validationResultCacheTree.getCachedValue(object); final VDiagnostic vDiagnostic = getVElement().getDiagnostic(); int highestSeverity = Diagnostic.OK; if (vDiagnostic != null) { for (final Diagnostic diagnostic : vDiagnostic.getDiagnostics(object)) { if (diagnostic.getSeverity() > highestSeverity) { highestSeverity = diagnostic.getSeverity(); } } } final ImageDescriptor overlay = SWTValidationHelper.INSTANCE.getValidationOverlayDescriptor(highestSeverity, getVElement(), getViewModelContext()); if (overlay == null) { return image; } final OverlayImageDescriptor imageDescriptor = new OverlayImageDescriptor(image, overlay, OverlayImageDescriptor.LOWER_RIGHT); final Image resultImage = imageDescriptor.createImage(); return resultImage; } } @Override protected void applyEnable() { // Re-select the current selection to enforce re-rendering the detail. treeViewer.setSelection(new StructuredSelection(treeViewer.getStructuredSelection().getFirstElement())); } /** * {@inheritDoc} * * @see org.eclipse.emfforms.spi.swt.core.AbstractSWTRenderer#applyValidation() * @since 1.14 */ @Override protected void applyValidation(final VDiagnostic oldDia, final VDiagnostic newDia) { super.applyValidation(); if (treeViewer == null) { return; } Display.getDefault().asyncExec(new Runnable() { @Override public void run() { if (treeViewer.getTree().isDisposed()) { return; } updateTree(oldDia, newDia); } }); } private void updateTree(VDiagnostic oldDia, VDiagnostic newDia) { final List<Object> diff = new ArrayList<Object>(); if (newDia != null) { diff.addAll(newDia.getDiagnostics()); } if (oldDia != null) { diff.removeAll(oldDia.getDiagnostics()); } final List<Object> diff2 = new ArrayList<Object>(); if (oldDia != null) { diff2.addAll(oldDia.getDiagnostics()); } if (newDia != null) { diff2.removeAll(newDia.getDiagnostics()); } diff.addAll(diff2); final Set<Object> toUpdate = new LinkedHashSet<Object>(); final ITreeContentProvider provider = ITreeContentProvider.class.cast(treeViewer.getContentProvider()); for (final Object o : diff) { final EObject toAdd = (EObject) Diagnostic.class.cast(o).getData().get(0); toUpdate.add(toAdd); Object parent = provider.getParent(toAdd); while (EObject.class.isInstance(parent)) { toUpdate.add(parent); parent = provider.getParent(parent); } } if (toUpdate.isEmpty() && !(oldDia.getDiagnostics().isEmpty() && newDia.getDiagnostics().isEmpty())) { treeViewer.refresh(); return; } treeViewer.update(toUpdate.toArray(), null); } /** * reference service for the detail pane of a tree master detail view. * It delegates calls to a root reference service, but opens new element in the detail pane. * * @author Jonas * */ private class TreeMasterDetailReferenceService implements ReferenceService { private final ReferenceService delegate; TreeMasterDetailReferenceService(ReferenceService delegate) { this.delegate = delegate; } /** * {@inheritDoc} * * @see org.eclipse.emf.ecp.view.spi.context.ViewModelService#instantiate(org.eclipse.emf.ecp.view.spi.context.ViewModelContext) */ @Override public void instantiate(ViewModelContext context) { // no op } /** * {@inheritDoc} * * @see org.eclipse.emf.ecp.view.spi.context.ViewModelService#dispose() */ @Override public void dispose() { // no op } /** * {@inheritDoc} * * @see org.eclipse.emf.ecp.view.spi.context.ViewModelService#getPriority() */ @Override public int getPriority() { if (delegate == null) { return 0; } return delegate.getPriority() - 1; } /** * {@inheritDoc} * * @see org.eclipse.emf.ecp.edit.spi.ReferenceService#addNewModelElements(org.eclipse.emf.ecore.EObject, * org.eclipse.emf.ecore.EReference) */ @Override public void addNewModelElements(EObject eObject, EReference eReference) { addNewModelElements(eObject, eReference, true); } /** * {@inheritDoc} * * @see org.eclipse.emf.ecp.edit.spi.ReferenceService#addNewModelElements(org.eclipse.emf.ecore.EObject, * org.eclipse.emf.ecore.EReference) */ @Override public Optional<EObject> addNewModelElements(EObject eObject, EReference eReference, boolean openInNewContext) { if (delegate == null) { return Optional.empty(); } return delegate.addNewModelElements(eObject, eReference, openInNewContext); } /** * {@inheritDoc} * * @see org.eclipse.emf.ecp.edit.spi.ReferenceService#addExistingModelElements(org.eclipse.emf.ecore.EObject, * org.eclipse.emf.ecore.EReference) */ @Override public void addExistingModelElements(EObject eObject, EReference eReference) { if (delegate == null) { return; } delegate.addExistingModelElements(eObject, eReference); } /** * {@inheritDoc} * * @see org.eclipse.emf.ecp.edit.spi.ReferenceService#openInNewContext(org.eclipse.emf.ecore.EObject) */ @Override public void openInNewContext(EObject eObject) { treeViewer.setSelection(new StructuredSelection(eObject), true); final ISelection selection = treeViewer.getSelection(); if (!selection.isEmpty()) { return; } if (delegate == null) { return; } delegate.openInNewContext(eObject); } } }