/*******************************************************************************
* Copyright (c) 2014, 2016 itemis AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Alexander Nyßen (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.mvc.fx.parts;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.gef.common.beans.property.ReadOnlyListWrapperEx;
import org.eclipse.gef.common.beans.property.ReadOnlySetMultimapProperty;
import org.eclipse.gef.common.beans.property.ReadOnlySetMultimapWrapper;
import org.eclipse.gef.common.collections.CollectionUtils;
import org.eclipse.gef.common.collections.ObservableSetMultimap;
import org.eclipse.gef.mvc.fx.viewer.IViewer;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
/**
* The {@link AbstractContentPart} is an {@link IContentPart} implementation
* that binds the VR type parameter (visual root type) to {@link Node}.
*
* @author anyssen
*
* @param <V>
* The visual {@link Node} used by this {@link AbstractContentPart} .
*/
public abstract class AbstractContentPart<V extends Node>
extends AbstractVisualPart<V> implements IContentPart<V> {
private final ObjectProperty<Object> contentProperty = new SimpleObjectProperty<>(
this, CONTENT_PROPERTY);
private ObservableList<Object> contentChildren = CollectionUtils
.observableArrayList();
private ObservableList<Object> contentChildrenUnmodifiable;
private ReadOnlyListWrapper<Object> contentChildrenUnmodifiableProperty;
private ObservableSetMultimap<Object, String> contentAnchorages = CollectionUtils
.observableHashMultimap();
private ObservableSetMultimap<Object, String> contentAnchoragesUnmodifiable;
private ReadOnlySetMultimapWrapper<Object, String> contentAnchoragesUnmodifiableProperty;
/**
* Creates a new {@link AbstractContentPart}.
*/
public AbstractContentPart() {
// XXX: Register the first listener on the content property, so
// registration is performed before all listeners are notified.
contentProperty.addListener(new ChangeListener<Object>() {
@Override
public void changed(ObservableValue<? extends Object> observable,
Object oldValue, Object newValue) {
onContentChanged(oldValue, newValue);
}
});
}
// TODO: Implement refresh(ITransformable), refresh(IResizable), and
// refresh(IBendable)
// @Override
// protected void doRefreshVisual(V visual) {
// if (this instanceof IBendable) {
// refresh((IBendable) this);
// } else {
// if (this instanceof ITransformable) {
// refresh((ITransformable) this);
// }
// if (this instanceof IResizable) {
// refresh((IResizable) this);
// }
// }
// }
/**
* {@inheritDoc}
* <p>
* Delegates to {@link #doAddContentChild(Object, int)}, which is to be
* overwritten by subclasses.
*/
@Override
public final void addContentChild(Object contentChild, int index) {
List<Object> oldContentChildren = new ArrayList<>(
doGetContentChildren());
if (oldContentChildren.contains(contentChild)) {
int oldIndex = oldContentChildren.indexOf(contentChild);
if (oldIndex == index) {
throw new IllegalArgumentException("Cannot add " + contentChild
+ " because its already contained at given index "
+ index);
} else {
throw new IllegalArgumentException("Cannot add " + contentChild
+ " because its already a content child at index "
+ oldIndex);
}
}
doAddContentChild(contentChild, index);
// check doAddContentChild(Object, int) does not violate postconditions
List<? extends Object> newContentChildren = doGetContentChildren();
if (!newContentChildren.contains(contentChild)) {
throw new IllegalStateException(
"doAddContentChild(Object, int) did not add content child "
+ contentChild + " .");
}
int newIndex = newContentChildren.indexOf(contentChild);
if (newIndex != index) {
throw new IllegalStateException(
"doAddContentChild(Object, int) did not add content child "
+ contentChild + " at index " + index
+ ", but at index " + newIndex + ".");
}
contentChildren.setAll(newContentChildren);
}
/**
* {@inheritDoc}
* <p>
* Delegates to {@link #doAttachToContentAnchorage(Object, String)}, which
* is to be overwritten by subclasses.
*/
@Override
public final void attachToContentAnchorage(Object contentAnchorage,
String role) {
SetMultimap<Object, String> oldContentAnchorages = HashMultimap
.create(doGetContentAnchorages());
if (oldContentAnchorages.containsEntry(contentAnchorage, role)) {
throw new IllegalArgumentException("Already attached to anchorage "
+ contentAnchorage + " in role '" + role + "'.");
}
doAttachToContentAnchorage(contentAnchorage, role);
// check doAttachToContentAnchorage(Object, String) does not violate
// postconditions
SetMultimap<Object, String> newContentAnchorages = HashMultimap
.create(doGetContentAnchorages());
if (!newContentAnchorages.containsEntry(contentAnchorage, role)) {
throw new IllegalArgumentException(
"doAttachToContentAnchorage did not properly attach to "
+ contentAnchorage + " with role '" + role + "'.");
}
// TODO: extract; is duplicate to code in detachFromContentAnchorages()
// ensure we have an atomic change per key
for (Object key : oldContentAnchorages.keySet()) {
if (newContentAnchorages.containsKey(key)) {
contentAnchorages.replaceValues(key,
newContentAnchorages.get(key));
} else {
contentAnchorages.removeAll(key);
}
}
for (Object key : newContentAnchorages.keySet()) {
if (!oldContentAnchorages.containsKey(key)) {
contentAnchorages.putAll(key, newContentAnchorages.get(key));
}
}
}
@Override
public ReadOnlySetMultimapProperty<Object, String> contentAnchoragesUnmodifiableProperty() {
if (contentAnchoragesUnmodifiableProperty == null) {
contentAnchoragesUnmodifiableProperty = new ReadOnlySetMultimapWrapper<>(
this, CONTENT_ANCHORAGES_PROPERTY,
getContentAnchoragesUnmodifiable());
}
return contentAnchoragesUnmodifiableProperty.getReadOnlyProperty();
}
@Override
public ReadOnlyListProperty<Object> contentChildrenUnmodifiableProperty() {
if (contentChildrenUnmodifiableProperty == null) {
contentChildrenUnmodifiableProperty = new ReadOnlyListWrapperEx<>(
this, CONTENT_CHILDREN_PROPERTY,
getContentChildrenUnmodifiable());
}
return contentChildrenUnmodifiableProperty.getReadOnlyProperty();
}
@Override
public final ObjectProperty<Object> contentProperty() {
return contentProperty;
}
/**
* {@inheritDoc}
* <p>
* Delegates to {@link #doDetachFromContentAnchorage(Object, String)}, which
* is to be overwritten by subclasses.
*/
@Override
public final void detachFromContentAnchorage(Object contentAnchorage,
String role) {
SetMultimap<Object, String> oldContentAnchorages = HashMultimap
.create(doGetContentAnchorages());
if (!oldContentAnchorages.containsEntry(contentAnchorage, role)) {
throw new IllegalArgumentException(
"Not attached to content anchorage " + contentAnchorage
+ " with role '" + role + "'.");
}
doDetachFromContentAnchorage(contentAnchorage, role);
// check postconditions for doDetachFromContentAnchorage(Object, String)
SetMultimap<Object, String> newContentAnchorages = HashMultimap
.create(doGetContentAnchorages());
if (newContentAnchorages.containsEntry(contentAnchorage, role)) {
throw new IllegalArgumentException(
"doDetachFromContentAnchorage did not properly detach from "
+ contentAnchorage + " with role '" + role + "'.");
}
// ensure we have an atomic change per key
for (Object key : oldContentAnchorages.keySet()) {
if (newContentAnchorages.containsKey(key)) {
contentAnchorages.replaceValues(key,
newContentAnchorages.get(key));
} else {
contentAnchorages.removeAll(key);
}
}
for (Object key : newContentAnchorages.keySet()) {
if (!oldContentAnchorages.containsKey(key)) {
contentAnchorages.putAll(key, newContentAnchorages.get(key));
}
}
}
/**
* Adds the given <i>contentChild</i> to this part's content children, so
* that it will no longer be returned by subsequent calls to
* {@link #doGetContentChildren()}.
*
* @param contentChild
* An {@link Object} which should be removed from this part's
* content children.
* @param index
* The index of the <i>contentChild</i> that is removed.
*/
protected void doAddContentChild(Object contentChild, int index) {
throw new UnsupportedOperationException(
"Need to implement doAddContentChild(Object, int) for "
+ this.getClass());
}
/**
* Attaches this part's content to the given <i>contentAnchorage</i> under
* the specified <i>role</i>, so that it will be returned by subsequent
* calls to {@link #doGetContentAnchorages()}.
*
* @param contentAnchorage
* An {@link Object} to which this part's content should be
* attached to.
* @param role
* The role under which the attachment is to be established.
*/
protected void doAttachToContentAnchorage(Object contentAnchorage,
String role) {
throw new UnsupportedOperationException(
"Need to implement doAttachContentChild(Object, String) for "
+ this.getClass());
}
/**
* Detaches this part's content from the given <i>contentAnchorage</i> under
* the specified <i>role</i>, so that it will no longer be returned by
* subsequent calls to {@link #doGetContentAnchorages()}.
*
* @param contentAnchorage
* An {@link Object} from which this part's content should be
* detached from.
* @param role
* The role under which the attachment is established.
*/
protected void doDetachFromContentAnchorage(Object contentAnchorage,
String role) {
throw new UnsupportedOperationException(
"Need to implement doDetachContentChild(Object, String) for "
+ this.getClass());
}
/**
* Hook method to return the current list of content anchorages. Has to be
* overwritten by clients.
*
* @return The current list of content anchorages.
*/
protected abstract SetMultimap<? extends Object, String> doGetContentAnchorages();
/**
* Hook method to return the current list of content children. Has to be
* overwritten by clients.
*
* @return The current list of content children.
*/
protected abstract List<? extends Object> doGetContentChildren();
/**
* Removes the given <i>contentChild</i> from this part's content children,
* so that it will no longer be returned by subsequent calls to
* {@link #doGetContentChildren()}.
*
* @param contentChild
* An {@link Object} which should be removed from this part's
* content children.
*/
protected void doRemoveContentChild(Object contentChild) {
throw new UnsupportedOperationException(
"Need to implement doRemoveContentChild(Object, int) for "
+ this.getClass());
}
/**
* Rearranges the given <i>contentChild</i> to the new index position.
*
* @param contentChild
* The {@link Object} which is to be reordered.
* @param newIndex
* The index to which the content child is to be reordered.
*/
protected void doReorderContentChild(Object contentChild, int newIndex) {
throw new UnsupportedOperationException(
"Need to implement doReorderContentChild(Object, int) for "
+ this.getClass());
}
/**
* @see IContentPart#getContent()
*/
@Override
public Object getContent() {
return contentProperty.get();
}
@Override
public ObservableSetMultimap<Object, String> getContentAnchoragesUnmodifiable() {
if (contentAnchoragesUnmodifiable == null) {
contentAnchoragesUnmodifiable = CollectionUtils
.unmodifiableObservableSetMultimap(contentAnchorages);
}
return contentAnchoragesUnmodifiable;
}
@Override
public ObservableList<Object> getContentChildrenUnmodifiable() {
if (contentChildrenUnmodifiable == null) {
contentChildrenUnmodifiable = FXCollections
.unmodifiableObservableList(contentChildren);
}
return contentChildrenUnmodifiable;
}
@Override
public boolean isFocusable() {
return true;
}
@Override
public boolean isSelectable() {
return true;
}
/**
* Called whenever the content of this {@link IContentPart} changed.
*
* @param oldContent
* The old content.
* @param newContent
* The new/current content.
*/
private void onContentChanged(Object oldContent, Object newContent) {
if (oldContent != null && oldContent != newContent) {
// unregister from content part map if we did not loose the
// viewer reference (otherwise we should already have
// removed ourselves)
if (getViewer() != null) {
unregisterFromContentPartMap(getViewer(), oldContent);
}
// clear content children and anchorages
if (newContent == null) {
contentChildren.clear();
contentAnchorages.clear();
}
}
if (newContent != null && newContent != oldContent) {
// if we have a viewer reference, register at content part
// map (otherwise do this as soon as we obtain the viewer
// reference)
if (getViewer() != null) {
registerAtContentPartMap(getViewer(), newContent);
}
// lazily initialize content children and anchorages
refreshContentChildren();
refreshContentAnchorages();
}
}
@Override
public void refreshContentAnchorages() {
// XXX: We use atomic operations here to replace the contents so we
// have minimal resulting change notifications.
contentAnchorages.replaceAll(doGetContentAnchorages());
}
@Override
public void refreshContentChildren() {
// XXX: We use atomic operations here to replace the contents so we
// have minimal resulting change notifications.
contentChildren.setAll(doGetContentChildren());
}
@Override
protected void register(IViewer viewer) {
super.register(viewer);
if (contentProperty.get() != null) {
registerAtContentPartMap(viewer, contentProperty.get());
}
}
/**
* Registers the <i>model</i> in the {@link IViewer#getContentPartMap()}.
* Subclasses should only extend this method if they need to register this
* EditPart in additional ways.
*
* @param viewer
* The viewer to register at.
*
* @param content
* The content to register.
*/
protected void registerAtContentPartMap(IViewer viewer, Object content) {
viewer.getContentPartMap().put(content, this);
}
/**
* {@inheritDoc}
* <p>
* Delegates to {@link #doRemoveContentChild(Object)}, which is to be
* overwritten by subclasses.
*/
@Override
public final void removeContentChild(Object contentChild) {
List<Object> oldContentChildren = new ArrayList<>(
doGetContentChildren());
if (!oldContentChildren.contains(contentChild)) {
throw new IllegalArgumentException("Cannot remove " + contentChild
+ " because its not a content child.");
}
doRemoveContentChild(contentChild);
// check doRemoveContentChild(Object, int) does not violate
// postconditions
List<? extends Object> newContentChildren = doGetContentChildren();
if (newContentChildren.contains(contentChild)) {
throw new IllegalStateException(
"doRemoveContentChild(Object, int) did not remove content child "
+ contentChild + " .");
}
contentChildren.setAll(newContentChildren);
}
/**
* {@inheritDoc}
* <p>
* Delegates to {@link #doReorderContentChild(Object, int)}, which is to be
* overwritten by subclasses.
*/
@Override
public void reorderContentChild(Object contentChild, int newIndex) {
List<Object> oldContentChildren = new ArrayList<>(
doGetContentChildren());
if (oldContentChildren.contains(contentChild)) {
throw new IllegalArgumentException("Cannot reorder " + contentChild
+ " because its not a content child.");
}
if (oldContentChildren.indexOf(contentChild) == newIndex) {
throw new IllegalArgumentException(
"Cannot reorder " + contentChild + " to given index + "
+ newIndex + ", because its already there.");
}
doReorderContentChild(contentChild, newIndex);
// check doReorderContentChild(Object, int) does not violate
// postconditions
List<? extends Object> newContentChildren = doGetContentChildren();
if (newContentChildren.indexOf(contentChild) != newIndex) {
throw new IllegalStateException(
"doReorderContentChild(Object, int) did not reorder content child "
+ contentChild + " to index " + newIndex + ".");
}
contentChildren.setAll(newContentChildren);
}
/**
* Set the primary content object that this EditPart represents. This method
* is used by an {@link IContentPartFactory} when creating an
* {@link IContentPart}.
*
* @see IContentPart#setContent(Object)
*/
@Override
public void setContent(Object content) {
this.contentProperty.set(content);
}
@Override
protected void unregister(IViewer viewer) {
// remove content children and anchorages
super.unregister(viewer);
if (getContent() != null) {
unregisterFromContentPartMap(viewer, getContent());
}
}
/**
* Unregisters the <i>model</i> in the {@link IViewer#getContentPartMap()}.
* Subclasses should only extend this method if they need to unregister this
* EditPart in additional ways.
*
* @param viewer
* The viewer to unregister from.
*
* @param content
* The content to unregister.
*/
protected void unregisterFromContentPartMap(IViewer viewer,
Object content) {
Map<Object, IContentPart<? extends Node>> registry = viewer
.getContentPartMap();
if (registry.get(content) != this) {
throw new IllegalArgumentException("Not registered under content");
}
registry.remove(content);
}
}