/******************************************************************************* * Copyright (c) 2017 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: * Matthias Wienand (itemis AG) - initial API and implementation * *******************************************************************************/ package org.eclipse.gef.mvc.fx.ui.actions; import org.eclipse.core.commands.operations.IOperationHistoryListener; import org.eclipse.core.commands.operations.OperationHistoryEvent; import org.eclipse.gef.fx.nodes.InfiniteCanvas; import org.eclipse.gef.mvc.fx.domain.HistoricizingDomain; import org.eclipse.gef.mvc.fx.ui.MvcFxUiBundle; import org.eclipse.jface.action.IAction; import org.eclipse.swt.widgets.Event; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.geometry.Bounds; import javafx.scene.Parent; import javafx.scene.transform.Affine; import javafx.scene.transform.TransformChangedEvent; /** * The {@link FitToViewportLockAction} is a specialized * {@link FitToViewportAction} that implements toggle functionality, i.e. when * checked, this action will perform fit-to-viewport for every viewport size * change until it is unchecked again. * * @author mwienand * */ public class FitToViewportLockAction extends FitToViewportAction { private boolean boundsChanged = false; private boolean sizeChanged = false; private boolean offsetChanged = false; private double savedContentBoundsWidth = 0d; private double savedContentBoundsHeight = 0d; private ReadOnlyObjectProperty<Bounds> contentBoundsProperty; private Affine contentTransform; private InfiniteCanvas infiniteCanvas; private boolean running; private EventHandler<TransformChangedEvent> trafoChangeListener = new EventHandler<TransformChangedEvent>() { @Override public void handle(TransformChangedEvent event) { // unlock when the user manually zooms setChecked(false); } }; private ChangeListener<? super Bounds> contentBoundsChangeListener = new ChangeListener<Bounds>() { @Override public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) { boundsChanged = true; } }; private ChangeListener<? super Number> scrollOffsetChangeListener = new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { offsetChanged = true; } }; private ChangeListener<? super Number> sizeChangeListener = new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { // fit-to-viewport when the user resizes the viewport sizeChanged = true; onSizeChanged(); } }; private IOperationHistoryListener historyListener = new IOperationHistoryListener() { @Override public void historyNotification(OperationHistoryEvent event) { if (event.getEventType() == OperationHistoryEvent.OPERATION_ADDED) { // flush changes Bounds contentBounds = infiniteCanvas.getContentBounds(); if (offsetChanged && (!boundsChanged || (contentBounds .getWidth() == savedContentBoundsWidth && contentBounds .getHeight() == savedContentBoundsHeight))) { // unlock upon manual scrolling setChecked(false); } else if (boundsChanged && !sizeChanged) { // fit-to-viewport otherwise onSizeChanged(); } // reset state boundsChanged = false; sizeChanged = false; offsetChanged = false; } } }; /** * Constructs a new {@link FitToViewportLockAction}. */ public FitToViewportLockAction() { super("Fit-To-Viewport Lock", IAction.AS_CHECK_BOX, MvcFxUiBundle.getDefault().getImageRegistry().getDescriptor( MvcFxUiBundle.IMG_ICONS_FIT_TO_VIEWPORT_LOCK)); } /** * Disables all viewport listeners that react to scroll offset, viewport * transformation, or viewport-/scrollable-/content-bounds changes. */ protected void disableViewportListeners() { infiniteCanvas.horizontalScrollOffsetProperty() .removeListener(scrollOffsetChangeListener); infiniteCanvas.verticalScrollOffsetProperty() .removeListener(scrollOffsetChangeListener); contentTransform.removeEventHandler( TransformChangedEvent.TRANSFORM_CHANGED, trafoChangeListener); contentBoundsProperty.removeListener(contentBoundsChangeListener); } /** * Enables all viewport listeners that react to scroll offset, viewport * transformation, or viewport-/scrollable-/content-bounds changes. * <p> * Moreover, stores the content bounds size, so that the size can later be * tested for changes. */ protected void enableViewportListeners() { infiniteCanvas.horizontalScrollOffsetProperty() .addListener(scrollOffsetChangeListener); infiniteCanvas.verticalScrollOffsetProperty() .addListener(scrollOffsetChangeListener); contentTransform.addEventHandler( TransformChangedEvent.TRANSFORM_CHANGED, trafoChangeListener); contentBoundsProperty.addListener(contentBoundsChangeListener); // save content bounds to detect scrolling savedContentBoundsWidth = infiniteCanvas.getContentBounds().getWidth(); savedContentBoundsHeight = infiniteCanvas.getContentBounds() .getHeight(); } /** * This method is called when this action needs to observe the viewport size * in order to perform fit-to-viewport if the viewport size changes. */ protected void lock() { // register history listener ((HistoricizingDomain) getViewer().getDomain()).getOperationHistory() .addOperationHistoryListener(historyListener); // register viewport size listeners Parent canvas = getViewer().getCanvas(); if (canvas instanceof InfiniteCanvas) { infiniteCanvas = (InfiniteCanvas) canvas; contentTransform = infiniteCanvas.getContentTransform(); contentBoundsProperty = infiniteCanvas.contentBoundsProperty(); enableViewportListeners(); infiniteCanvas.widthProperty().addListener(sizeChangeListener); infiniteCanvas.heightProperty().addListener(sizeChangeListener); } } /** * This method is called when the viewport size was changed. It performs * fit-to-viewport if this action is enabled. */ protected void onSizeChanged() { // only called when locked if (isEnabled()) { runWithEvent(null); } } @Override protected void register() { super.register(); if (isChecked()) { lock(); // initial fit-to-viewport runWithEvent(null); } } @Override public void runWithEvent(Event event) { // FIXME: Prevent re-entrance by properly disabling listeners instead of // guarding against re-entrance here by using the 'running' flag. if (this.running) { return; } this.running = true; if (isChecked()) { disableViewportListeners(); super.runWithEvent(event); enableViewportListeners(); } this.running = false; } @Override public void setChecked(boolean checked) { if (isEnabled()) { if (isChecked() && !checked) { unlock(); } else if (!isChecked() && checked) { lock(); } } super.setChecked(checked); } /** * This method is called when this action does no longer need to observe the * viewport size, because no further fit-to-viewport should be performed if * the viewport size changes. */ protected void unlock() { // remove history listener ((HistoricizingDomain) getViewer().getDomain()).getOperationHistory() .removeOperationHistoryListener(historyListener); // unregister viewport size listeners if (infiniteCanvas != null) { disableViewportListeners(); infiniteCanvas.widthProperty().removeListener(sizeChangeListener); infiniteCanvas.heightProperty().removeListener(sizeChangeListener); } // reset state boundsChanged = false; sizeChanged = false; offsetChanged = false; } @Override protected void unregister() { unlock(); super.unregister(); } }