/******************************************************************************* * 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 * Camille Letavernier (camille.letavernier@cea.fr) - fix for bug #475399 * * Note: Parts of this class have been transferred from org.eclipse.gef.SelectionManager. * *******************************************************************************/ package org.eclipse.gef.mvc.fx.models; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.eclipse.gef.common.beans.property.ReadOnlyListWrapperEx; import org.eclipse.gef.common.collections.CollectionUtils; import org.eclipse.gef.common.dispose.IDisposable; import org.eclipse.gef.mvc.fx.parts.IContentPart; import org.eclipse.gef.mvc.fx.parts.IVisualPart; import org.eclipse.gef.mvc.fx.viewer.IViewer; import javafx.beans.property.ReadOnlyListProperty; import javafx.beans.property.ReadOnlyListWrapper; import javafx.collections.FXCollections; import javafx.collections.MapChangeListener; import javafx.collections.ObservableList; import javafx.scene.Node; /** * The {@link SelectionModel} is used to store the current viewer's selection. * It represents the selection as an ordered list of {@link IContentPart}s. * Thereby, it supports a multi-selection and allows to identify a primary * selection (the head element of the list) that may be treated specially. * * @author anyssen * @author mwienand * */ // TODO: We could expose the selection as modifiable collection and modifiable // read-only property if we could use an ordered set. As we use a list, we have // to ensure it does not contain duplicates. public class SelectionModel extends org.eclipse.gef.common.adapt.IAdaptable.Bound.Impl<IViewer> implements IDisposable { /** * Name of the {@link #selectionUnmodifiableProperty()}. */ public static final String SELECTION_PROPERTY = "selection"; private ObservableList<IContentPart<? extends Node>> selection = CollectionUtils .observableArrayList(); private ObservableList<IContentPart<? extends Node>> selectionUnmodifiable = FXCollections .unmodifiableObservableList(selection); private ReadOnlyListWrapper<IContentPart<? extends Node>> selectionUnmodifiableProperty = new ReadOnlyListWrapperEx<>( this, SELECTION_PROPERTY, selectionUnmodifiable); private MapChangeListener<Node, IVisualPart<? extends Node>> visualPartMapListener = new MapChangeListener<Node, IVisualPart<? extends Node>>() { @Override public void onChanged( javafx.collections.MapChangeListener.Change<? extends Node, ? extends IVisualPart<? extends Node>> change) { // keep model in sync with part hierarchy if (change.wasRemoved()) { IVisualPart<? extends Node> valueRemoved = change .getValueRemoved(); if (selection.contains(valueRemoved)) { selection.remove(valueRemoved); } } } }; /** * Updates the current selection by adding the given {@link IContentPart} to * it, preserving already selected elements. * <p> * If the given content part is not already selected, it will be added to * the back of the given selection, otherwise it will be moved to the back. * A member of the current selection that is not contained in the given * list, will remain selected. * * @param toBeAppended * The {@link IContentPart} to add to/move to the back of the * current selection. */ public void appendToSelection(IContentPart<? extends Node> toBeAppended) { appendToSelection(Collections.singletonList(toBeAppended)); } /** * Updates the current selection by adding the given {@link IContentPart}s * to it, preserving already selected elements. * <p> * A member of the given list that is not contained in the current * selection, will be added to it. A member of the current selection that is * not contained in the given list, will remain selected. * <p> * The selection order will be adjusted, so that the members of the given * list are added at the back (in the order they are given), preceded by the * already selected elements not contained in the given list (preserving * their relative order). * * @param toBeAppended * The {@link IContentPart}s to add to/move to the back of the * current selection. */ public void appendToSelection( List<? extends IContentPart<? extends Node>> toBeAppended) { List<IContentPart<? extends Node>> newSelection = getSelectionCopy(); newSelection.removeAll(toBeAppended); for (IContentPart<? extends Node> p : toBeAppended) { if (newSelection.contains(p)) { throw new IllegalArgumentException("The content part " + p + " is provided more than once in the given list."); } newSelection.add(p); } // XXX: ObservableList.setAll() is not properly guarded against not // having an effect (and will always notify attached listeners) if (!selection.equals(newSelection)) { selection.setAll(newSelection); } } /** * Clears the current selection. */ public void clearSelection() { selection.clear(); } /** * @since 1.1 */ @Override public void dispose() { // setAdaptable() already clears the selection } /** * Returns a modifiable list of the currently selected {@link IContentPart} * s. * * @return A modifiable list of the currently selected {@link IContentPart} * s. */ private List<IContentPart<? extends Node>> getSelectionCopy() { return new ArrayList<>(selection); } /** * Returns an unmodifiable observable list of the currently selected * {@link IContentPart}s. * * @return An unmodifiable observable list of the currently selected * {@link IContentPart}s. */ public ObservableList<IContentPart<? extends Node>> getSelectionUnmodifiable() { return selectionUnmodifiable; } /** * Returns whether the given {@link IContentPart} is part of the current * selection. * * @param contentPart * The {@link IContentPart} which is checked for containment. * @return <code>true</code> if the {@link IContentPart} is contained by the * current selection. */ public boolean isSelected(IContentPart<? extends Node> contentPart) { return selection.contains(contentPart); } /** * Updates the current selection by adding the given {@link IContentPart} to * it, preserving already selected elements. * <p> * If the given content part is not already selected, it will be added to * the front of the given selection, otherwise it will be moved to the * front. A member of the current selection that is not contained in the * given list, will remain selected. * * @param toBePrepended * The {@link IContentPart} to add to/move to the front of the * current selection. */ public void prependToSelection(IContentPart<? extends Node> toBePrepended) { prependToSelection(Collections.singletonList(toBePrepended)); } /** * Updates the current selection by adding the given {@link IContentPart}s * to it, preserving already selected elements. * <p> * A member of the given list that is not contained in the current * selection, will be added to it. A member of the current selection that is * not contained in the given list, will remain selected. * <p> * The selection order will be adjusted, so that the members of the given * list are added in front (in the order they are given), followed by the * already selected elements not contained in the given list (preserving * their relative order). * * @param toBePrepended * The {@link IContentPart}s to add to/move to the front of the * current selection. */ public void prependToSelection( List<? extends IContentPart<? extends Node>> toBePrepended) { List<IContentPart<? extends Node>> newSelection = getSelectionCopy(); newSelection.removeAll(toBePrepended); int i = 0; for (IContentPart<? extends Node> p : toBePrepended) { if (newSelection.contains(p)) { throw new IllegalArgumentException("The content part " + p + " is provided more than once in the given list."); } newSelection.add(i++, p); } if (!selection.equals(newSelection)) { selection.setAll(newSelection); } } /** * Removes the given {@link IContentPart}s from the current selection if * they are contained. Ignores those that are not part of the current * selection. * * @param contentParts * The {@link IContentPart}s which are removed from the * selection. */ public void removeFromSelection( Collection<? extends IContentPart<? extends Node>> contentParts) { selection.removeAll(contentParts); } /** * Removes the given {@link IContentPart} from the current selection if it * is currently selected. Will not change the current selection otherwise. * * @param contentPart * The {@link IContentPart} that is to be removed from the * selection. */ public void removeFromSelection(IContentPart<? extends Node> contentPart) { selection.remove(contentPart); } /** * Returns an unmodifiable read-only list property that represents the * current selection. * * @return An unmodifiable read-only property named * {@link #SELECTION_PROPERTY}. */ public ReadOnlyListProperty<IContentPart<? extends Node>> selectionUnmodifiableProperty() { return selectionUnmodifiableProperty.getReadOnlyProperty(); } @Override public void setAdaptable(IViewer adaptable) { if (getAdaptable() != null) { // unregister visual-part-map listener getAdaptable().visualPartMapProperty() .removeListener(visualPartMapListener); } super.setAdaptable(adaptable); if (adaptable != null) { // register for visual-part-map changes adaptable.visualPartMapProperty() .addListener(visualPartMapListener); } // start with a clean SelectionModel clearSelection(); } /** * Replaces the current selection with the given {@link IContentPart}. * * @param newSelection * The {@link IContentPart} constituting the new selection. */ public void setSelection(IContentPart<? extends Node> newSelection) { setSelection(Collections.singletonList(newSelection)); } /** * Replaces the current selection with the given list of * {@link IContentPart} s. * * @param selection * The list of {@link IContentPart}s constituting the new * selection. */ public void setSelection( List<? extends IContentPart<? extends Node>> selection) { List<IContentPart<? extends Node>> newSelection = new ArrayList<>(); int i = 0; for (IContentPart<? extends Node> p : selection) { if (newSelection.contains(p)) { throw new IllegalArgumentException("The content part " + p + " is provided more than once in the given list."); } newSelection.add(i++, p); } if (!this.selection.equals(newSelection)) { this.selection.setAll(newSelection); } } }