/******************************************************************************* * 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.viewer; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Map; import java.util.TreeMap; import org.eclipse.gef.common.activate.ActivatableSupport; import org.eclipse.gef.common.activate.IActivatable; import org.eclipse.gef.common.adapt.AdaptableSupport; import org.eclipse.gef.common.adapt.AdapterKey; import org.eclipse.gef.common.adapt.inject.InjectAdapters; import org.eclipse.gef.common.beans.property.ReadOnlyListWrapperEx; import org.eclipse.gef.common.beans.property.ReadOnlyMapWrapperEx; import org.eclipse.gef.common.collections.CollectionUtils; import org.eclipse.gef.fx.nodes.InfiniteCanvas; import org.eclipse.gef.fx.utils.NodeUtils; import org.eclipse.gef.mvc.fx.domain.IDomain; import org.eclipse.gef.mvc.fx.parts.IContentPart; import org.eclipse.gef.mvc.fx.parts.IRootPart; import org.eclipse.gef.mvc.fx.parts.IVisualPart; import com.google.common.reflect.TypeToken; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyListProperty; import javafx.beans.property.ReadOnlyListWrapper; import javafx.beans.property.ReadOnlyMapProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Window; /** * The {@link InfiniteCanvasViewer} is an {@link IViewer} that manages an * {@link InfiniteCanvas} to display the viewer's contents. * * @author anyssen */ public class InfiniteCanvasViewer implements IViewer { private InfiniteCanvas infiniteCanvas; private boolean isInitialized = false; private boolean isWindowFocused = false; private boolean isFocusOwnerFocused = false; private ReadOnlyBooleanWrapper viewerFocusedProperty = new ReadOnlyBooleanWrapper( false); private ObservableList<Object> contents = CollectionUtils .observableArrayList(); private ReadOnlyListWrapper<Object> contentsProperty = new ReadOnlyListWrapperEx<>( this, CONTENTS_PROPERTY, contents); private BooleanBinding viewerFocusedPropertyBinding = new BooleanBinding() { @Override protected boolean computeValue() { return isWindowFocused && isFocusOwnerFocused; } }; private ChangeListener<Window> windowObserver = new ChangeListener<Window>() { @Override public void changed(ObservableValue<? extends Window> observable, Window oldValue, Window newValue) { onWindowChanged(oldValue, newValue); } }; private ChangeListener<Boolean> windowFocusedObserver = new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { onWindowFocusedChanged(oldValue, newValue); } }; private ChangeListener<Node> focusOwnerObserver = new ChangeListener<Node>() { @Override public void changed(ObservableValue<? extends Node> observable, Node oldValue, Node newValue) { if (oldValue != newValue) { onFocusOwnerChanged(oldValue, newValue); } } }; private ChangeListener<Boolean> focusOwnerFocusedObserver = new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { // XXX: If a new focusOwner is set and the old focusOwner was // focused, it will fire a "focused" change from true to false. Such // events are disregarded, so that the viewer does not lose its // focus if it will gain it immediately afterwards (which is handled // within #focusOwnerChanged()). if (observable == getCanvas().getScene().getFocusOwner()) { if (oldValue == null ? newValue != null : !oldValue.equals(newValue)) { onFocusOwnerFocusedChanged(oldValue, newValue); } } } }; private ChangeListener<Scene> sceneListener = new ChangeListener<Scene>() { @Override public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) { onSceneChanged(oldValue, newValue); } }; private ActivatableSupport acs = new ActivatableSupport(this); private AdaptableSupport<IViewer> ads = new AdaptableSupport<>(this); // XXX: Use HashMap for contentPartMap so that equals() is used for // containment tests, which is also used when working with lists. The // implementation needs to match the implementation that is used within // ContentBehavior. private ObservableMap<Object, IContentPart<? extends Node>> contentPartMap = FXCollections .observableMap(new HashMap<>()); private ReadOnlyMapProperty<Object, IContentPart<? extends Node>> contentPartMapProperty; private ObservableMap<Node, IVisualPart<? extends Node>> visualPartMap = FXCollections .observableMap(new IdentityHashMap<>()); private ReadOnlyMapProperty<Node, IVisualPart<? extends Node>> visualPartMapProperty; private ReadOnlyObjectWrapper<IDomain> domainProperty = new ReadOnlyObjectWrapper<>(); /** * Creates a new {@link InfiniteCanvasViewer}. */ public InfiniteCanvasViewer() { super(); // add binding to viewer focused property to have its value computed // based on the values of: // - window focused // - focusOwner // - focusOwner focused viewerFocusedProperty.bind(viewerFocusedPropertyBinding); } @Override public final void activate() { acs.activate(null, this::doActivate); } /** * Activates the adapters registered at this {@link InfiniteCanvasViewer}. */ protected void activateAdapters() { // XXX: We keep a sorted map of adapters so activation // is performed in a deterministic order new TreeMap<>(ads.getAdapters()).values().forEach((adapter) -> { if (adapter instanceof IActivatable) { ((IActivatable) adapter).activate(); } }); } @Override public final ReadOnlyBooleanProperty activeProperty() { return acs.activeProperty(); } @Override public ReadOnlyObjectProperty<IDomain> adaptableProperty() { return domainProperty.getReadOnlyProperty(); } @Override public ReadOnlyMapProperty<AdapterKey<?>, Object> adaptersProperty() { return ads.adaptersProperty(); } @Override public ReadOnlyMapProperty<Object, IContentPart<? extends Node>> contentPartMapProperty() { if (contentPartMapProperty == null) { contentPartMapProperty = new ReadOnlyMapWrapperEx<>(this, CONTENT_PART_MAP_PROPERTY, contentPartMap); } return contentPartMapProperty; } @Override public ReadOnlyListProperty<Object> contentsProperty() { return contentsProperty.getReadOnlyProperty(); } @Override public final void deactivate() { acs.deactivate(this::doDeactivate, null); } /** * Deactivates the adapters registered at this {@link InfiniteCanvasViewer}. */ protected void deactivateAdapters() { // XXX: We keep a sorted map of adapters so deactivation // is performed in a deterministic order new TreeMap<>(ads.getAdapters()).values().forEach((adapter) -> { if (adapter instanceof IActivatable) { ((IActivatable) adapter).deactivate(); } }); } @Override public void dispose() { // ensure all listeners are properly unregistered if (infiniteCanvas != null) { if (infiniteCanvas.getScene() != null) { onSceneChanged(infiniteCanvas.getScene(), null); } infiniteCanvas.sceneProperty().removeListener(sceneListener); infiniteCanvas = null; } // unbind viewer focused property viewerFocusedProperty.unbind(); viewerFocusedProperty = null; // dispose adapters (including root part and models) ads.dispose(); ads = null; // clear content part map if (!contentPartMap.isEmpty()) { throw new IllegalStateException( "Content part map was not properly cleared!"); } contentPartMap = null; // clear visual part map if (!visualPartMap.isEmpty()) { throw new IllegalStateException( "Visual part map was not properly cleared!"); } visualPartMap = null; // unset activatable support acs = null; } /** * Activates this {@link InfiniteCanvasViewer}, which activates its * adapters. */ protected void doActivate() { if (getDomain() == null) { throw new IllegalStateException( "Domain has to be set before activation."); } if (getRootPart() == null) { throw new IllegalStateException( "RootPart has to be set before activation."); } if (infiniteCanvas == null || infiniteCanvas.getScene() == null) { throw new IllegalStateException( "Viewer controls have to be hooked (to scene) before activation."); } activateAdapters(); } /** * Deactivates this {@link InfiniteCanvasViewer}, which deactivates its * adapters. */ protected void doDeactivate() { deactivateAdapters(); } @Override public IDomain getAdaptable() { return domainProperty.get(); } @Override public <T> T getAdapter(AdapterKey<T> key) { return ads.getAdapter(key); } @Override public <T> T getAdapter(Class<T> classKey) { return ads.getAdapter(classKey); } @Override public <T> T getAdapter(TypeToken<T> key) { return ads.getAdapter(key); } @Override public <T> AdapterKey<T> getAdapterKey(T adapter) { return ads.getAdapterKey(adapter); } @Override public ObservableMap<AdapterKey<?>, Object> getAdapters() { return ads.getAdapters(); } @Override public <T> Map<AdapterKey<? extends T>, T> getAdapters( Class<? super T> classKey) { return ads.getAdapters(classKey); } @Override public <T> Map<AdapterKey<? extends T>, T> getAdapters( TypeToken<? super T> key) { return ads.getAdapters(key); } /** * Returns the {@link InfiniteCanvas} that is managed by this * {@link InfiniteCanvasViewer} . * * @return The {@link InfiniteCanvas} that is managed by this * {@link InfiniteCanvasViewer} . */ @Override public InfiniteCanvas getCanvas() { if (infiniteCanvas == null) { infiniteCanvas = new InfiniteCanvas(); infiniteCanvas.sceneProperty().addListener(sceneListener); // hook root visual IRootPart<? extends Node> rootPart = getRootPart(); infiniteCanvas.getContentGroup().getChildren() .addAll((Parent) rootPart.getVisual()); } return infiniteCanvas; } /** * @see IViewer#getContentPartMap() */ @Override public Map<Object, IContentPart<? extends Node>> getContentPartMap() { return contentPartMap; } @Override public ObservableList<Object> getContents() { return contents; } @Override public IDomain getDomain() { return domainProperty.get(); } @SuppressWarnings("serial") @Override public IRootPart<? extends Node> getRootPart() { return ads.getAdapter(new TypeToken<IRootPart<? extends Node>>() { }); } /** * Returns the {@link Scene} in which the {@link InfiniteCanvas} of this * {@link InfiniteCanvasViewer} is displayed. * * @return The {@link Scene} in which the {@link InfiniteCanvas} of this * {@link InfiniteCanvasViewer} is displayed. */ public Scene getScene() { return getCanvas().getScene(); } /** * @see IViewer#getVisualPartMap() */ @Override public Map<Node, IVisualPart<? extends Node>> getVisualPartMap() { return visualPartMap; } @Override public final boolean isActive() { return acs.isActive(); } @Override public boolean isViewerFocused() { return viewerFocusedProperty.get(); } private void onFocusOwnerChanged(Node oldFocusOwner, Node newFocusOwner) { if (oldFocusOwner != null && NodeUtils.isNested(getCanvas(), oldFocusOwner)) { oldFocusOwner.focusedProperty() .removeListener(focusOwnerFocusedObserver); } if (newFocusOwner != null && NodeUtils.isNested(getCanvas(), newFocusOwner)) { newFocusOwner.focusedProperty() .addListener(focusOwnerFocusedObserver); // check if viewer is focused if (Boolean.TRUE.equals(newFocusOwner.focusedProperty().get())) { isFocusOwnerFocused = true; viewerFocusedPropertyBinding.invalidate(); } } else { // viewer unfocused isFocusOwnerFocused = false; viewerFocusedPropertyBinding.invalidate(); } } private void onFocusOwnerFocusedChanged(Boolean oldValue, Boolean newValue) { isFocusOwnerFocused = Boolean.TRUE.equals(newValue); viewerFocusedPropertyBinding.invalidate(); } private void onSceneChanged(Scene oldScene, Scene newScene) { Window oldWindow = null; Window newWindow = null; Node oldFocusOwner = null; Node newFocusOwner = null; if (oldScene != null) { oldWindow = oldScene.windowProperty().get(); oldScene.windowProperty().removeListener(windowObserver); oldFocusOwner = oldScene.focusOwnerProperty().get(); oldScene.focusOwnerProperty().removeListener(focusOwnerObserver); } if (newScene != null) { newWindow = newScene.windowProperty().get(); newScene.windowProperty().addListener(windowObserver); newFocusOwner = newScene.focusOwnerProperty().get(); newScene.focusOwnerProperty().addListener(focusOwnerObserver); } onWindowChanged(oldWindow, newWindow); onFocusOwnerChanged(oldFocusOwner, newFocusOwner); } private void onWindowChanged(Window oldValue, Window newValue) { if (oldValue != null) { oldValue.focusedProperty().removeListener(windowFocusedObserver); } if (newValue != null) { newValue.focusedProperty().addListener(windowFocusedObserver); // check if window is focused if (Boolean.TRUE.equals(newValue.focusedProperty().get())) { isWindowFocused = true; viewerFocusedPropertyBinding.invalidate(); } } else { // window unfocused isInitialized = false; isWindowFocused = false; viewerFocusedPropertyBinding.invalidate(); } } private void onWindowFocusedChanged(Boolean oldValue, Boolean newValue) { isWindowFocused = Boolean.TRUE.equals(newValue); viewerFocusedPropertyBinding.invalidate(); if (!isInitialized) { // XXX: When the embedded scene is opened, the viewer needs to // request focus for the root visual once so that a focus owner is // set. This could also possibly be done in the FocusBehavior, but // keeping it here we can limit 'knowledge' about the embedded // window. getRootPart().getVisual().requestFocus(); isInitialized = true; } } @Override public void reveal(IVisualPart<? extends Node> visualPart) { if (visualPart == null) { getCanvas().setHorizontalScrollOffset(0); getCanvas().setVerticalScrollOffset(0); } else { getCanvas().reveal(visualPart.getVisual()); } } @Override public void setAdaptable(IDomain domain) { domainProperty.set(domain); } @Override public <T> void setAdapter(T adapter) { ads.setAdapter(adapter); } @Override public <T> void setAdapter(T adapter, String role) { ads.setAdapter(adapter, role); } @Override public <T> void setAdapter(TypeToken<T> adapterType, T adapter) { ads.setAdapter(adapterType, adapter); } @InjectAdapters @Override public <T> void setAdapter(TypeToken<T> adapterType, T adapter, String role) { ads.setAdapter(adapterType, adapter, role); } @Override public <T> void unsetAdapter(T adapter) { ads.unsetAdapter(adapter); } @Override public ReadOnlyBooleanProperty viewerFocusedProperty() { return viewerFocusedProperty.getReadOnlyProperty(); } @Override public ReadOnlyMapProperty<Node, IVisualPart<? extends Node>> visualPartMapProperty() { if (visualPartMapProperty == null) { visualPartMapProperty = new ReadOnlyMapWrapperEx<>(this, VISUAL_PART_MAP_PROPERTY, visualPartMap); } return visualPartMapProperty; } }