/******************************************************************************* * 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 * * Note: Parts of this class have been transferred from org.eclipse.gef.editparts.AbstractEditPart. * *******************************************************************************/ package org.eclipse.gef.mvc.fx.behaviors; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.eclipse.gef.common.collections.SetMultimapChangeListener; import org.eclipse.gef.common.dispose.IDisposable; import org.eclipse.gef.mvc.fx.parts.IContentPart; import org.eclipse.gef.mvc.fx.parts.IContentPartFactory; import org.eclipse.gef.mvc.fx.parts.IRootPart; import org.eclipse.gef.mvc.fx.parts.IVisualPart; import org.eclipse.gef.mvc.fx.parts.PartUtils; import org.eclipse.gef.mvc.fx.viewer.IViewer; import com.google.common.collect.HashMultimap; import com.google.common.collect.Maps; import com.google.common.collect.SetMultimap; import javafx.beans.property.ReadOnlyProperty; import javafx.collections.ListChangeListener; import javafx.collections.MapChangeListener; import javafx.scene.Node; /** * A behavior that can be adapted to an {@link IRootPart} or an * {@link IContentPart} to synchronize the list of {@link IContentPart} children * and (only in case of an {@link IContentPart}) anchorages with the list of * content children and anchored. * * @author anyssen * */ public class ContentBehavior extends AbstractBehavior implements IDisposable { private ListChangeListener<Object> contentObserver = new ListChangeListener<Object>() { @Override public void onChanged( ListChangeListener.Change<? extends Object> change) { // System.out.println("Content changed " + change); // XXX: An atomic operation (including setAll()) on the // ObservableList will lead to an atomic change here; we do not have // to iterate through the individual changes but may simply // synchronize with the list as it emerges after the changes have // been applied. // while (change.next()) { // if (change.wasRemoved()) { // removeContentPartChildren(getHost(), // ImmutableList.copyOf(change.getRemoved())); // } else if (change.wasAdded()) { // addContentPartChildren(getHost(), // ImmutableList.copyOf(change.getAddedSubList()), // change.getFrom()); // } else if (change.wasPermutated()) { // throw new UnsupportedOperationException( // "Reorder not yet implemented"); // } // } synchronizeContentPartChildren(getHost(), change.getList()); } }; private ListChangeListener<Object> contentChildrenObserver = new ListChangeListener<Object>() { @SuppressWarnings("unchecked") @Override public void onChanged( final ListChangeListener.Change<? extends Object> change) { // System.out.println("Content children changed " + change); // XXX: An atomic operation (including setAll()) on the // ObservableList will lead to an atomic change here; we do not have // to iterate through the individual changes but may simply // synchronize with the list as it emerges after the changes have // been applied. IContentPart<? extends Node> parent = (IContentPart<? extends Node>) ((ReadOnlyProperty<?>) change .getList()).getBean(); // while (change.next()) { // if (change.wasRemoved()) { // removeContentPartChildren(parent, // ImmutableList.copyOf(change.getRemoved())); // } else if (change.wasAdded()) { // addContentPartChildren(parent, // ImmutableList.copyOf(change.getAddedSubList()), // change.getFrom()); // } else if (change.wasPermutated()) { // throw new UnsupportedOperationException( // "Reorder not yet implemented"); // } // } synchronizeContentPartChildren(parent, change.getList()); } }; private SetMultimapChangeListener<Object, String> contentAnchoragesObserver = new SetMultimapChangeListener<Object, String>() { @SuppressWarnings("unchecked") @Override public void onChanged( final SetMultimapChangeListener.Change<? extends Object, ? extends String> change) { // System.out.println("Content anchorages changed " + change); // XXX: An atomic operation (including replaceAll()) on the // ObservableSetMultimap will lead to an atomic change here; we do // not have to iterate through the individual changes but may simply // synchronize with the list as it emerges after the changes have // been applied. // TODO: detach or attach directly IContentPart<? extends Node> anchored = (IContentPart<? extends Node>) ((ReadOnlyProperty<?>) change .getSetMultimap()).getBean(); synchronizeContentPartAnchorages(anchored, HashMultimap.create(change.getSetMultimap())); } }; private MapChangeListener<Object, IContentPart<? extends Node>> contentPartMapObserver = new MapChangeListener<Object, IContentPart<? extends Node>>() { @Override public void onChanged( MapChangeListener.Change<? extends Object, ? extends IContentPart<? extends Node>> change) { if (change.wasRemoved()) { IContentPart<? extends Node> contentPart = change .getValueRemoved(); contentPart.contentChildrenUnmodifiableProperty() .removeListener(contentChildrenObserver); contentPart.contentAnchoragesUnmodifiableProperty() .removeListener(contentAnchoragesObserver); } if (change.wasAdded()) { IContentPart<? extends Node> contentPart = change .getValueAdded(); contentPart.contentChildrenUnmodifiableProperty() .addListener(contentChildrenObserver); contentPart.contentAnchoragesUnmodifiableProperty() .addListener(contentAnchoragesObserver); } } }; @SuppressWarnings("unchecked") private List<IContentPart<? extends Node>> addAll( IVisualPart<? extends Node> parent, List<? extends Object> contentChildren) { List<IContentPart<? extends Node>> childContentParts = PartUtils .filterParts(parent.getChildrenUnmodifiable(), IContentPart.class); List<IContentPart<? extends Node>> added = new ArrayList<>(); // store the existing content parts in a map using the contents as keys Map<Object, IContentPart<? extends Node>> contentPartMap = new HashMap<>(); // find all content parts for which no content element exists in // contentChildren, and therefore have to be removed for (IContentPart<? extends Node> contentPart : childContentParts) { // store content part in map contentPartMap.put(contentPart.getContent(), contentPart); } int contentChildrenSize = contentChildren.size(); int childContentPartsSize = childContentParts.size(); for (int i = 0; i < contentChildrenSize; i++) { Object content = contentChildren.get(i); // Do a quick check to see if the existing content part is at // the correct location in the children list. if (i < childContentPartsSize && childContentParts.get(i).getContent() == content) { continue; } // Look to see if the ContentPart is already around but in the // wrong location. IContentPart<? extends Node> contentPart = findOrCreatePartFor( content); if (contentPartMap.containsKey(content)) { // Re-order the existing content part to its designated // location in the children list. // TODO: this is wrong, it has to take into consideration // the visual parts in between parent.reorderChild(contentPart, i); } else { // A ContentPart for this model does not exist yet. Create // and insert one. if (contentPart.getParent() != null) { // TODO: Up to now a model element may only be // controlled by a single content part; unless we // differentiate content elements by context (which is not // covered by the current content part map implementation) // it is an illegal state if we locate a content part, which // is already bound to a parent and whose content is equal // to the one we are processing here. throw new IllegalStateException( "Located a ContentPart which controls the same (or an equal) content element but is already bound to a parent. A content element may only be controlled by a single ContentPart."); } parent.addChild(contentPart, i); added.add(contentPart); added.addAll(addAll(contentPart, contentPart.getContentChildrenUnmodifiable())); } } return added; } @SuppressWarnings("unchecked") private List<IContentPart<? extends Node>> detachAll( IVisualPart<? extends Node> parent, final List<? extends Object> contentChildren) { List<IContentPart<? extends Node>> toRemove = new ArrayList<>(); // only synchronize IContentPart children // find all content parts for which no content element exists in // contentChildren, and therefore have to be removed for (IContentPart<? extends Node> contentPart : (List<IContentPart<? extends Node>>) PartUtils .filterParts(parent.getChildrenUnmodifiable(), IContentPart.class)) { // mark for removal if (!contentChildren.contains(contentPart.getContent())) { toRemove.addAll( detachAll(contentPart, Collections.emptyList())); toRemove.add(contentPart); synchronizeContentPartAnchorages(contentPart, HashMultimap.create()); } } return toRemove; } @Override public void dispose() { // the content part pool is shared by all content behaviors of a viewer, // so the viewer disposes it. contentObserver = null; contentChildrenObserver = null; contentAnchoragesObserver = null; } /** * If the given {@link IContentPart} does neither have a parent nor any * anchoreds, then it's content is set to <code>null</code> and the part is * added to the {@link ContentPartPool}. * * @param contentPart * The {@link IContentPart} that is eventually disposed. */ protected void disposeIfObsolete(IContentPart<? extends Node> contentPart) { if (contentPart.getParent() == null && contentPart.getAnchoredsUnmodifiable().isEmpty()) { // System.out.println("DISPOSE " + contentPart.getContent()); getContentPartPool().add(contentPart); contentPart.setContent(null); } // else { // System.out.println("CANNOT DISPOSE " + contentPart.getContent()); // } } @Override protected void doActivate() { IVisualPart<? extends Node> host = getHost(); if (host != host.getRoot()) { throw new IllegalArgumentException(); } IViewer viewer = host.getRoot().getViewer(); viewer.contentPartMapProperty().addListener(contentPartMapObserver); synchronizeContentPartChildren(getHost(), viewer.getContents()); viewer.getContents().addListener(contentObserver); } @Override protected void doDeactivate() { IVisualPart<? extends Node> host = getHost(); IViewer viewer = host.getRoot().getViewer(); viewer.getContents().removeListener(contentObserver); synchronizeContentPartChildren(getHost(), Collections.emptyList()); viewer.contentPartMapProperty().removeListener(contentPartMapObserver); } /** * Finds/Revives/Creates an {@link IContentPart} for the given * <i>content</i> {@link Object}. If an {@link IContentPart} for the given * content {@link Object} can be found in the viewer's content-part-map, * then this part is returned. If an {@link IContentPart} for the given * content {@link Object} is stored in the {@link ContentPartPool}, then * this part is returned. Otherwise, the injected * {@link IContentPartFactory} is used to create a new {@link IContentPart} * for the given content {@link Object}. * * @param content * The content {@link Object} for which the corresponding * {@link IContentPart} is to be returned. * @return The {@link IContentPart} corresponding to the given * <i>content</i> {@link Object}. */ protected IContentPart<? extends Node> findOrCreatePartFor(Object content) { Map<Object, IContentPart<? extends Node>> contentPartMap = getHost() .getRoot().getViewer().getContentPartMap(); if (contentPartMap.containsKey(content)) { // System.out.println("FOUND " + content); return contentPartMap.get(content); } else { // 'Revive' a content part, if it was removed before IContentPart<? extends Node> contentPart = getContentPartPool() .remove(content); // If the part could not be revived, a new one is created if (contentPart == null) { // create part using the factory // System.out.println("CREATE " + content); IContentPartFactory contentPartFactory = getContentPartFactory(); contentPart = contentPartFactory.createContentPart(content, Collections.emptyMap()); if (contentPart == null) { throw new IllegalStateException("IContentPartFactory '" + contentPartFactory.getClass().getSimpleName() + "' did not create part for " + content + "."); } } // else { // System.out.println("REVIVE " + content); // } // initialize part contentPart.setContent(content); return contentPart; } } /** * Returns the {@link IContentPartFactory} of the current viewer. * * @return the {@link IContentPartFactory} of the current viewer. */ protected IContentPartFactory getContentPartFactory() { return getHost().getRoot().getViewer() .getAdapter(IContentPartFactory.class); } /** * Returns the {@link ContentPartPool} that is used to recycle content parts * in the context of an {@link IViewer}. * * @return The {@link ContentPartPool} of the {@link IViewer}. */ protected ContentPartPool getContentPartPool() { return getHost().getRoot().getViewer() .getAdapter(ContentPartPool.class); } /** * Updates the host {@link IVisualPart}'s {@link IContentPart} anchorages * (see {@link IVisualPart#getAnchoragesUnmodifiable()}) so that it is in * sync with the set of content anchorages that is passed in. * * @param anchored * The anchored {@link IVisualPart} whose content part anchorages * to synchronize with the given content anchorages. * * @param contentAnchorages * * The map of content anchorages with roles to be synchronized * with the list of {@link IContentPart} anchorages ( * {@link IContentPart#getAnchoragesUnmodifiable()}). * * @see IContentPart#getContentAnchoragesUnmodifiable() * @see IContentPart#getAnchoragesUnmodifiable() */ public void synchronizeContentPartAnchorages( IVisualPart<? extends Node> anchored, SetMultimap<? extends Object, ? extends String> contentAnchorages) { if (contentAnchorages == null) { throw new IllegalArgumentException( "contentAnchorages may not be null"); } SetMultimap<IVisualPart<? extends Node>, String> anchorages = anchored .getAnchoragesUnmodifiable(); // find anchorages whose content vanished List<Entry<IVisualPart<? extends Node>, String>> toRemove = new ArrayList<>(); Set<Entry<IVisualPart<? extends Node>, String>> entries = anchorages .entries(); for (Entry<IVisualPart<? extends Node>, String> e : entries) { if (!(e.getKey() instanceof IContentPart)) { continue; } Object content = ((IContentPart<? extends Node>) e.getKey()) .getContent(); if (!contentAnchorages.containsEntry(content, e.getValue())) { toRemove.add(e); } } // Correspondingly remove the anchorages. This is done in a separate // step to prevent ConcurrentModificationException. for (Entry<IVisualPart<? extends Node>, String> contentPart : toRemove) { anchored.detachFromAnchorage(contentPart.getKey(), contentPart.getValue()); disposeIfObsolete( (IContentPart<? extends Node>) contentPart.getKey()); } // find content for which no anchorages exist List<Entry<IVisualPart<? extends Node>, String>> toAdd = new ArrayList<>(); for (Entry<? extends Object, ? extends String> e : contentAnchorages .entries()) { IContentPart<? extends Node> anchorage = findOrCreatePartFor( e.getKey()); if (!anchorages.containsEntry(anchorage, e.getValue())) { toAdd.add( Maps.<IVisualPart<? extends Node>, String> immutableEntry( anchorage, e.getValue())); } } // Correspondingly add the anchorages. This is done in a separate // step to prevent ConcurrentModificationException. for (Entry<IVisualPart<? extends Node>, String> e : toAdd) { anchored.attachToAnchorage(e.getKey(), e.getValue()); } } /** * Updates the host {@link IVisualPart}'s {@link IContentPart} children (see * {@link IVisualPart#getChildrenUnmodifiable()}) so that it is in sync with * the set of content children that is passed in. * * @param parent * The parent {@link IVisualPart} whose content part children to * synchronize against the given content children. * * @param contentChildren * The list of content part children to be synchronized with the * list of {@link IContentPart} children ( * {@link IContentPart#getChildrenUnmodifiable()}). * * @see IContentPart#getContentChildrenUnmodifiable() * @see IContentPart#getChildrenUnmodifiable() */ public void synchronizeContentPartChildren( IVisualPart<? extends Node> parent, final List<? extends Object> contentChildren) { if (contentChildren == null) { throw new IllegalArgumentException( "contentChildren may not be null"); } List<IContentPart<? extends Node>> toRemove = detachAll(parent, contentChildren); for (IContentPart<? extends Node> contentPart : toRemove) { contentPart.getParent().removeChild(contentPart); disposeIfObsolete(contentPart); } List<IContentPart<? extends Node>> added = addAll(parent, contentChildren); for (IContentPart<? extends Node> cp : added) { synchronizeContentPartAnchorages(cp, cp.getContentAnchoragesUnmodifiable()); } } }