blob: cfea73a5d0b211831c66c9d13b1aa93eea474909 [file] [log] [blame]
/**
********************************************************************************
* Copyright (c) 2021 Robert Bosch GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* See4sys - initial API and implementation (Sphinx)
* itemis AG - adaptation to Amalthea
* Robert Bosch GmbH - implementation without Sphinx
********************************************************************************
*/
package org.eclipse.app4mc.amalthea.model.provider;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import org.eclipse.app4mc.amalthea.model.AmaltheaFactory;
import org.eclipse.app4mc.amalthea.model.AmaltheaPackage;
import org.eclipse.core.runtime.Assert;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CommandWrapper;
import org.eclipse.emf.common.command.CompoundCommand;
import org.eclipse.emf.common.command.UnexecutableCommand;
import org.eclipse.emf.common.notify.Adapter;
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.util.ResourceLocator;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.FeatureMap;
import org.eclipse.emf.ecore.util.FeatureMapUtil;
import org.eclipse.emf.edit.command.AddCommand;
import org.eclipse.emf.edit.command.CommandParameter;
import org.eclipse.emf.edit.command.CreateChildCommand;
import org.eclipse.emf.edit.command.MoveCommand;
import org.eclipse.emf.edit.command.SetCommand;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.provider.AttributeValueWrapperItemProvider;
import org.eclipse.emf.edit.provider.DelegatingWrapperItemProvider;
import org.eclipse.emf.edit.provider.Disposable;
import org.eclipse.emf.edit.provider.FeatureMapEntryWrapperItemProvider;
import org.eclipse.emf.edit.provider.IEditingDomainItemProvider;
import org.eclipse.emf.edit.provider.IItemLabelProvider;
import org.eclipse.emf.edit.provider.IItemPropertySource;
import org.eclipse.emf.edit.provider.IStructuredItemContentProvider;
import org.eclipse.emf.edit.provider.ITreeItemContentProvider;
import org.eclipse.emf.edit.provider.ItemProviderAdapter;
/**
* A base class for transient item provider {@link Adapter adapter}s that can be
* used to insert non-model view objects between an object and its children.
* <p>
* Transient item providers must be instantiated statefully wrt their target
* object, i.e., the same transient item provider instance must not be used for
* multiple target objects.
* </p>
*/
public abstract class TransientItemProvider extends ItemProviderAdapter implements IEditingDomainItemProvider,
IStructuredItemContentProvider, ITreeItemContentProvider, IItemLabelProvider, IItemPropertySource {
public static class AdapterFactoryHelper {
// Suppress default constructor
private AdapterFactoryHelper() {
throw new IllegalStateException("Utility class");
}
public static Object adapt(Object target, Object type, AdapterFactory adapterFactory) {
if (type instanceof Class<?> && TransientItemProvider.class.isAssignableFrom((Class<?>) type)) {
Adapter adapter = EcoreUtil.getExistingAdapter((Notifier) target, type);
if (adapter != null) {
return adapter;
}
return adapterFactory.adaptNew((Notifier) target, type);
}
return null;
}
}
protected TransientItemProvider(AdapterFactory adapterFactory) {
super(adapterFactory);
}
/*
* Overridden to ensure statefulness of transient item provider adapters wrt
* their target object, i.e., to avoid that the same transient item provider
* instance is used for multiple target objects.
*
* @see
* org.eclipse.emf.edit.provider.ItemProviderAdapter#setTarget(org.eclipse.emf.
* common.notify.Notifier)
*/
@Override
public void setTarget(Notifier target) {
Assert.isLegal(this.target == null || this.target == target);
super.setTarget(target);
}
/*
* Overridden to ensure that an already existing instance of some transient item
* provider on a given target object to be retrieved by using the class of the
* transient item provider in question as type to adapt to (rather than the item
* provider adapter factory as intended by ItemProviderAdapter). In combination
* with TransientItemProvider.AdapterFactoryHelper#adapt(Object, Object,
* AdapterFactory), this behavior enables the implementation of item provider
* adapter factories that support the creation and retrieval transient item
* provider instances by calling AdapterFactory#adapt(object,
* TransientItemProviderSubClass.class)
*
* @see
* org.eclipse.emf.edit.provider.ItemProviderAdapter#isAdapterForType(java.lang.
* Object)
*
* @see org.eclipse.emf.ecore.util.EcoreUtil#getExistingAdapter(Notifier,
* Object)
*
* @see
* org.eclipse.sphinx.emf.edit.TransientItemProvider.AdapterFactoryHelper.adapt(
* Object, Object, AdapterFactory)
*/
@Override
public boolean isAdapterForType(Object type) {
return type == this.getClass();
}
/**
* Handles model change notifications by calling {@link #updateChildren} to
* update any cached children and by creating a viewer notification, which it
* passes to {@link #fireNotifyChanged} to trigger a full or partial refresh of
* the underlying viewer.
*/
@Override
public void notifyChanged(Notification notification) {
updateChildren(notification);
super.notifyChanged(notification);
}
/**
* This sets the parent of the transient item provider to <code>target</code>
* value. The target instance variable comes from the adapter base class
* {@link org.eclipse.emf.common.notify.impl.AdapterImpl}.
*/
@Override
public Object getParent(Object object) {
return target;
}
/**
* Returns a folder kind of icon as default image for non-model view objects
* between an object and its children.
*/
@Override
public Object getImage(Object object) {
if (object != null) {
return overlayImage(object, ExtendedAmaltheaEditPlugin.INSTANCE.getPluginResourceLocator()
.getImage("full/obj16/folder_closed"));
}
return null;
}
/*
* Overridden to use the target object behind this transient item provider
* rather than given object (which would be the transient item provider itself)
* in order to (1) retrieve the store for the children of the given object (and
* thereby retrieve it in the same way as in #updateChildren(Notification)) and
* (2) obtain the values of the children features.
*
* @see org.eclipse.emf.edit.provider.ItemProviderAdapter#getChildren(java.lang.
* Object)
*/
@Override
public Collection<?> getChildren(Object object) {
ChildrenStore store = getChildrenStore(target);
if (store != null) {
return store.getChildren();
}
store = createChildrenStore(object);
List<Object> result = store != null ? null : new ArrayList<>();
EObject eObject = (EObject) target;
for (EStructuralFeature feature : getChildrenFeatures(object)) {
if (feature.isMany()) {
List<?> children = (List<?>) eObject.eGet(feature);
int index = 0;
for (Object unwrappedChild : children) {
Object child = wrap(object, feature, unwrappedChild, index);
if (store != null) {
store.getList(feature).add(child);
} else {
result.add(child);
}
index++;
}
} else {
Object child = eObject.eGet(feature);
if (child != null) {
child = wrap(object, feature, child, CommandParameter.NO_INDEX);
if (store != null) {
store.setValue(feature, child);
} else {
result.add(child);
}
}
}
}
return store != null ? store.getChildren() : result;
}
/*
* Overridden to use the target object behind this transient item provider
* rather than given object (which would be the transient item provider itself)
* as key for the store for the children of the given object (and thereby match
* the way it is retrieved in #updateChildren(Notification)).
*
* @see
* org.eclipse.emf.edit.provider.ItemProviderAdapter#createChildrenStore(java.
* lang.Object)
*/
@Override
protected ChildrenStore createChildrenStore(Object object) {
ChildrenStore store = null;
if (isWrappingNeeded(object)) {
if (childrenStoreMap == null) {
childrenStoreMap = new HashMap<Object, ChildrenStore>();
}
store = new ChildrenStore(getChildrenFeatures(object));
childrenStoreMap.put(target, store);
}
return store;
}
@Override
protected Collection<? extends EStructuralFeature> getChildrenFeatures(final Object object) {
if (this.childrenFeatures == null) {
super.getChildrenFeatures(object);
this.childrenFeatures.addAll(myFeatures());
}
return this.childrenFeatures;
}
/**
* This implements delegated command creation for the given transient item
* provider and sets its owner to <code>target</code>.
*/
@Override
public Command createCommand(Object object, EditingDomain domain, Class<? extends Command> commandClass,
CommandParameter commandParameter) {
if (commandClass == CreateChildCommand.class || commandClass == MoveCommand.class) {
commandParameter.setOwner(target);
}
return super.createCommand(object, domain, commandClass, commandParameter);
}
/**
* This implements {@link IEditingDomainItemProvider#getNewChildDescriptors
* IEditingDomainItemProvider.getNewChildDescriptors}, returning descriptors for
* all the possible children that can be added to the specified
* <code>target</code>. The target instance variable comes from the adapter base
* class {@link org.eclipse.emf.common.notify.impl.AdapterImpl}.
*/
@Override
public Collection<?> getNewChildDescriptors(Object object, EditingDomain editingDomain, Object sibling) {
return super.getNewChildDescriptors(target, editingDomain, sibling);
}
/**
* Creates and returns a wrapper for the given value, at the given index in the
* given feature of the given object if such a wrapper is needed; otherwise,
* returns the original value.
* <p>
* This method is very similar to
* {@link #createWrapper(EObject, EStructuralFeature, Object, int)} but accepts
* and handles {@link Object} rather than just {@link EObject}
* <code>object</code> arguments.
* </p>
*
* @see #createWrapper(EObject, EStructuralFeature, Object, int)
*/
protected Object createWrapper(Object object, EStructuralFeature feature, Object value, int index) {
if (!isWrappingNeeded(object)) {
return value;
}
if (object instanceof EObject) {
if (FeatureMapUtil.isFeatureMap(feature)) {
value = new FeatureMapEntryWrapperItemProvider((FeatureMap.Entry) value, (EObject) object,
(EAttribute) feature, index, adapterFactory, getResourceLocator());
} else if (feature instanceof EAttribute) {
value = new AttributeValueWrapperItemProvider(value, (EObject) object, (EAttribute) feature, index,
adapterFactory, getResourceLocator());
}
} else {
if (!((EReference) feature).isContainment()) {
value = new DelegatingWrapperItemProvider(value, object, feature, index, adapterFactory);
}
}
return value;
}
/*
* Overridden to enable wrapping of cross-referenced model objects by default.
* This is required to make sure that the latter get represented by instances of
* org.eclipse.emf.edit.provider.DelegatingWrapperItemProvider and may have a
* parent element that is different from object containing them.
*
* @see
* org.eclipse.emf.edit.provider.ItemProviderAdapter#isWrappingNeeded(java.lang.
* Object)
*/
@Override
protected boolean isWrappingNeeded(Object object) {
if (wrappingNeeded == null) {
wrappingNeeded = Boolean.FALSE;
for (EStructuralFeature feature : getChildrenFeatures(object)) {
if (feature instanceof EAttribute
|| feature instanceof EReference && !((EReference) feature).isContainment()) {
wrappingNeeded = Boolean.TRUE;
break;
}
}
}
return wrappingNeeded;
}
/**
* Wraps a value, if needed, and keeps the wrapper for disposal along with the
* item provider. This method actually calls {@link #createWrapper
* createWrapper} to determine if the given value, at the given index in the
* given feature of the given object, should be wrapped and to obtain the
* wrapper. If a wrapper is obtained, it is recorded and returned. Otherwise,
* the original value is returned. Subclasses may override {@link #createWrapper
* createWrapper} to specify when and with what to wrap values.
* <p>
* This method is very similar to
* {@link #wrap(EObject, EStructuralFeature, Object, int)} but accepts and
* handles {@link Object} rather than just {@link EObject} <code>object</code>
* arguments.
* </p>
*/
protected Object wrap(Object object, EStructuralFeature feature, Object value, int index) {
if (!feature.isMany() && index != CommandParameter.NO_INDEX) {
throw new IllegalArgumentException("Bad wrap index.");
}
Object wrapper = createWrapper(object, feature, value, index);
if (wrapper == null) {
wrapper = value;
} else if (wrapper != value) {
if (wrappers == null) {
wrappers = new Disposable();
}
wrappers.add(wrapper);
}
return wrapper;
}
/**
* This allows creating a command and overriding the
* {@link org.eclipse.emf.common.command.CommandWrapper.getAffectedObjects()#getAffectedObjects()}
* method to return the appropriate transient item provider whenever the real
* affected object is the owner.
*
* @param command an command.
* @param owner the owner of the object.
* @return a {@link CommandWrapper}
*/
protected Command createWrappedCommand(Command command, final EObject owner) {
return new CommandWrapper(command) {
@Override
public Collection<?> getAffectedObjects() {
Collection<?> affected = super.getAffectedObjects();
if (affected.contains(owner)) {
affected = Collections.singleton(TransientItemProvider.this);
}
return affected;
}
};
}
/**
* This creates a primitive {@link org.eclipse.emf.edit.command.AddCommand}.
*/
@Override
protected Command createAddCommand(EditingDomain domain, EObject owner, EStructuralFeature feature,
Collection<?> collection, int index) {
return createWrappedCommand(super.createAddCommand(domain, owner, feature, collection, index), owner);
}
/**
* This creates a primitive {@link org.eclipse.emf.edit.command.RemoveCommand}.
*/
@Override
protected Command createRemoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature,
Collection<?> collection) {
return createWrappedCommand(super.createRemoveCommand(domain, owner, feature, collection), owner);
}
/*
* Overridden to support the case where specified owner via CommandParameter
* (commandParameter.getEOwner()) is no EObject. In this case, try to refer to
* parent or owner of specified owner.
*
* @see org.eclipse.emf.edit.provider.ItemProviderAdapter#factorAddCommand(org.
* eclipse.emf.edit.domain.EditingDomain,
* org.eclipse.emf.edit.command.CommandParameter)
*/
@Override
protected Command factorAddCommand(EditingDomain domain, CommandParameter commandParameter) {
if (commandParameter.getCollection() == null || commandParameter.getCollection().isEmpty()) {
return UnexecutableCommand.INSTANCE;
}
EObject eOwner = commandParameter.getEOwner();
// Try to refer to parent or owner in case that owner specified via
// CommandParameter is no EObject
if (eOwner == null) {
Object parentOfOwner = domain.getParent(commandParameter.getOwner());
if (parentOfOwner instanceof EObject) {
eOwner = (EObject) parentOfOwner;
}
}
final EObject eObject = eOwner;
final List<Object> list = new ArrayList<>(commandParameter.getCollection());
int index = commandParameter.getIndex();
CompoundCommand addCommand = new CompoundCommand(CompoundCommand.MERGE_COMMAND_ALL);
while (!list.isEmpty()) {
Iterator<Object> children = list.listIterator();
final Object firstChild = children.next();
EStructuralFeature childFeature = getChildFeature(eObject, firstChild);
if (childFeature == null) {
break;
}
// If it is a list type value...
//
else if (childFeature.isMany()) {
// Correct the index, if necessary.
//
if (index != CommandParameter.NO_INDEX) {
for (EStructuralFeature feature : getChildrenFeatures(commandParameter.getOwner())) {
if (feature == childFeature) {
break;
}
if (feature.isMany()) {
index -= ((List<?>) eObject.eGet(feature)).size();
} else if (eObject.eGet(feature) != null) {
index -= 1;
}
}
if (index < 0) {
break;
}
}
// These will be the children belonging to this feature.
//
Collection<Object> childrenOfThisFeature = new ArrayList<>();
childrenOfThisFeature.add(firstChild);
children.remove();
// Consume the rest of the appropriate children.
//
while (children.hasNext()) {
Object child = children.next();
// Is this child in this feature...
//
if (getChildFeature(eObject, child) == childFeature) {
// Add it to the list and remove it from the other list.
//
childrenOfThisFeature.add(child);
children.remove();
}
}
// Create a command for this feature,
//
addCommand.append(createAddCommand(domain, eObject, childFeature, childrenOfThisFeature, index));
if (index >= childrenOfThisFeature.size()) {
index -= childrenOfThisFeature.size();
} else {
index = CommandParameter.NO_INDEX;
}
} else if (eObject.eGet(childFeature) == null) {
Command setCommand = createSetCommand(domain, eObject, childFeature, firstChild);
addCommand.append(new CommandWrapper(setCommand) {
protected Collection<?> affected;
@Override
public void execute() {
super.execute();
affected = Collections.singleton(firstChild);
}
@Override
public void undo() {
super.undo();
affected = Collections.singleton(eObject);
}
@Override
public void redo() {
super.redo();
affected = Collections.singleton(firstChild);
}
@Override
public Collection<?> getResult() {
return Collections.singleton(firstChild);
}
@Override
public Collection<?> getAffectedObjects() {
return affected;
}
});
children.remove();
} else {
break;
}
}
// If all the objects aren't used up by the above, then we can't do the command.
//
if (list.isEmpty()) {
return addCommand.unwrap();
} else {
addCommand.dispose();
return UnexecutableCommand.INSTANCE;
}
}
/*
* Overridden to add support for the case where: (1) collection objects of given
* command parameter have different containers, or the same container but
* different features and (2) specified owner via CommandParameter
* (commandParameter.getEOwner()) is no EObject. In this case, try to refer to
* parent or owner of specified owner.
*
* @see
* org.eclipse.emf.edit.provider.ItemProviderAdapter#factorRemoveCommand(org.
* eclipse.emf.edit.domain.EditingDomain,
* org.eclipse.emf.edit.command.CommandParameter)
*/
@Override
protected Command factorRemoveCommand(EditingDomain domain, CommandParameter commandParameter) {
if (commandParameter.getCollection() == null || commandParameter.getCollection().isEmpty()) {
return UnexecutableCommand.INSTANCE;
}
EObject eOwner = commandParameter.getEOwner();
// Try to refer to parent or owner in case that owner specified via
// CommandParameter is no EObject
if (eOwner == null) {
Object parentOfOwner = domain.getParent(commandParameter.getOwner());
if (parentOfOwner instanceof EObject) {
eOwner = (EObject) parentOfOwner;
}
}
final EObject eObject = eOwner;
final List<Object> list = new ArrayList<>(commandParameter.getCollection());
CompoundCommand removeCommand = new CompoundCommand(CompoundCommand.MERGE_COMMAND_ALL);
// Iterator over all the child references to factor each child to the right
// reference.
//
for (EStructuralFeature feature : getChildrenFeatures(commandParameter.getOwner())) {
// If it is a list type value...
//
if (feature.isMany()) {
List<?> value = (List<?>) getFeatureValue(eObject, feature);
// These will be the children belonging to this feature.
//
Collection<Object> childrenOfThisFeature = new ArrayList<>();
for (ListIterator<Object> objects = list.listIterator(); objects.hasNext();) {
Object o = objects.next();
// Is this object in this feature...
//
if (value.contains(o)) {
// Add it to the list and remove it from the other list.
//
childrenOfThisFeature.add(o);
objects.remove();
}
}
// If we have children to remove for this feature, create a command for it.
//
if (!childrenOfThisFeature.isEmpty()) {
removeCommand.append(createRemoveCommand(domain, eObject, feature, childrenOfThisFeature));
}
} else {
// It's just a single value
//
final Object value = getFeatureValue(eObject, feature);
for (ListIterator<Object> objects = list.listIterator(); objects.hasNext();) {
Object o = objects.next();
// Is this object in this feature...
//
if (o == value) {
// Create a command to unset this and remove the object from the other list.
//
Command setCommand = createSetCommand(domain, eObject, feature, SetCommand.UNSET_VALUE);
removeCommand.append(new CommandWrapper(setCommand) {
protected Collection<?> affected;
@Override
public void execute() {
super.execute();
affected = Collections.singleton(eObject);
}
@Override
public void undo() {
super.undo();
affected = Collections.singleton(value);
}
@Override
public void redo() {
super.redo();
affected = Collections.singleton(eObject);
}
@Override
public Collection<?> getResult() {
return Collections.singleton(value);
}
@Override
public Collection<?> getAffectedObjects() {
return affected;
}
});
objects.remove();
break;
}
}
}
}
// If all the objects are used up by the above, then we can't do the command.
//
if (list.isEmpty()) {
return removeCommand.unwrap();
} else {
// FIXME File bug to EMF: In the case where objects in the list have different
// container, or the same
// container but different features we must to iterate over all container
// features
for (Object object : new ArrayList<Object>(list)) {
if (object instanceof EObject) {
final EObject fallBackOwner = ((EObject) object).eContainer();
EStructuralFeature containingFeature = ((EObject) object).eContainingFeature();
if (containingFeature != null) {
if (containingFeature.isMany()) {
List<?> value = (List<?>) getFeatureValue(fallBackOwner, containingFeature);
// These will be the children belonging to this feature.
//
Collection<Object> childrenOfThisFeature = new ArrayList<>();
for (ListIterator<Object> objects = list.listIterator(); objects.hasNext();) {
Object o = objects.next();
// Is this object in this feature...
//
if (value.contains(o)) {
// Add it to the list and remove it from the other list.
//
childrenOfThisFeature.add(o);
objects.remove();
}
}
// If we have children to remove for this feature, create a command for it.
//
if (!childrenOfThisFeature.isEmpty()) {
removeCommand.append(createRemoveCommand(domain, fallBackOwner, containingFeature,
childrenOfThisFeature));
}
} else {
// It's just a single value
//
final Object value = getFeatureValue(fallBackOwner, containingFeature);
for (ListIterator<Object> objects = list.listIterator(); objects.hasNext();) {
Object o = objects.next();
// Is this object in this feature...
//
if (o == value) {
// Create a command to set this to null and remove the object from the other
// list.
//
Command setCommand = domain.createCommand(SetCommand.class,
new CommandParameter(fallBackOwner, containingFeature, null));
removeCommand.append(new CommandWrapper(setCommand) {
protected Collection<?> affected;
@Override
public void execute() {
affected = Collections.singleton(
fallBackOwner.eContainer() != null ? fallBackOwner.eContainer()
: fallBackOwner);
super.execute();
}
@Override
public void undo() {
super.undo();
affected = Collections.singleton(value);
}
@Override
public void redo() {
super.redo();
affected = Collections.singleton(
fallBackOwner.eContainer() != null ? fallBackOwner.eContainer()
: fallBackOwner);
}
@Override
public Collection<?> getResult() {
return Collections.singleton(value);
}
@Override
public Collection<?> getAffectedObjects() {
return affected;
}
});
objects.remove();
break;
}
}
}
}
}
}
}
if (list.isEmpty()) {
return removeCommand.unwrap();
} else {
removeCommand.dispose();
return UnexecutableCommand.INSTANCE;
}
}
@Override
protected Command createDragAndDropCommand(final EditingDomain domain, final Object owner, final float location,
final int operations, final int operation, final Collection<?> collection) {
EObject dropTarget = null;
if (owner == this && target instanceof EObject) {
// for this transient item provider -> use (my) target
dropTarget = (EObject) target;
} else if (owner instanceof EObject) {
// use EObject
dropTarget = (EObject) owner;
}
for (EStructuralFeature feature : myFeatures()) {
if (dropTarget != null && new AddCommand(domain, dropTarget, feature, collection).canExecute()) {
return super.createDragAndDropCommand(domain, dropTarget, location, operations, operation, collection);
}
}
return UnexecutableCommand.INSTANCE;
}
protected AmaltheaPackage myPackage() {
return AmaltheaPackage.eINSTANCE;
}
protected AmaltheaFactory myFactory() {
return AmaltheaFactory.eINSTANCE;
}
@Override
protected ResourceLocator getResourceLocator() {
return AmaltheaEditPlugin.INSTANCE.getPluginResourceLocator();
}
// Default implementations.
// Subclasses should override either myFeature() or myFeatures().
public EStructuralFeature myFeature() {
return null;
}
public List<EStructuralFeature> myFeatures() {
return Collections.singletonList(myFeature());
}
}