diff --git a/jfoenix/src/main/java/com/jfoenix/bindings/CustomBidirectionalBinding.java b/jfoenix/src/main/java/com/jfoenix/bindings/CustomBidirectionalBinding.java new file mode 100644 index 00000000..46ef8742 --- /dev/null +++ b/jfoenix/src/main/java/com/jfoenix/bindings/CustomBidirectionalBinding.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.bindings; + +import com.jfoenix.bindings.base.IBiBinder; +import com.jfoenix.bindings.base.IPropertyConverter; +import javafx.beans.property.Property; +import javafx.beans.property.ReadOnlyProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.util.Callback; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.function.Consumer; + +/** + * Custom bidirectional binder, used for bidirectional binding between properties of different types + * or with different accessibility methods (e.g {@link ReadOnlyProperty} with setter method) + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2020-04-30 + */ +public class CustomBidirectionalBinding implements IBiBinder { + + private final WeakReference> propertyRef1; + private final WeakReference> propertyRef2; + private final Consumer propertyRef1Setter; + private final Consumer propertyRef2Setter; + private HashMap, ChangeListener> listeners = new HashMap<>(); + private IPropertyConverter converter; + + public CustomBidirectionalBinding(Property a, Property b, IPropertyConverter converter) { + this(a, value -> a.setValue(value), + b, value -> b.setValue(value), + converter); + } + + public CustomBidirectionalBinding(ReadOnlyProperty a, Consumer propertyRef1Setter, + ReadOnlyProperty b, Consumer propertyRef2Setter, + IPropertyConverter converter) { + this.propertyRef1 = new WeakReference<>(a); + this.propertyRef2 = new WeakReference<>(b); + this.propertyRef1Setter = propertyRef1Setter; + this.propertyRef2Setter = propertyRef2Setter; + this.converter = converter; + } + + @SuppressWarnings("unchecked") + @Override + public void unbindBi() { + listeners.entrySet().forEach(entry -> entry.getKey().removeListener(entry.getValue())); + } + + @Override + public void bindBi() { + addFlaggedChangeListener(propertyRef1.get(), propertyRef1Setter, propertyRef2.get(), propertyRef2Setter, param -> converter.to(param)); + addFlaggedChangeListener(propertyRef2.get(), propertyRef2Setter, propertyRef1.get(), propertyRef1Setter, param -> converter.from(param)); + propertyRef2Setter.accept(converter.to(propertyRef1.get().getValue())); + } + + private void addFlaggedChangeListener(ReadOnlyProperty a, Consumer aConsumer, + ReadOnlyProperty b, Consumer bConsumer, + Callback updateB) { + ChangeListener listener = new ChangeListener() { + private boolean alreadyCalled = false; + + @Override + public void changed(ObservableValue observable, a oldValue, a newValue) { + if (alreadyCalled) { + return; + } + try { + alreadyCalled = true; + bConsumer.accept(updateB.call(newValue)); + } finally { + alreadyCalled = false; + } + } + }; + listeners.put(a, listener); + a.addListener(listener); + } +} diff --git a/jfoenix/src/main/java/com/jfoenix/bindings/base/IBiBinder.java b/jfoenix/src/main/java/com/jfoenix/bindings/base/IBiBinder.java new file mode 100644 index 00000000..b303f372 --- /dev/null +++ b/jfoenix/src/main/java/com/jfoenix/bindings/base/IBiBinder.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.bindings.base; + +/** + * Custom bidirectional binder interface + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2020-04-30 + */ +public interface IBiBinder { + void unbindBi(); + + void bindBi(); +} diff --git a/jfoenix/src/main/java/com/jfoenix/bindings/base/IPropertyConverter.java b/jfoenix/src/main/java/com/jfoenix/bindings/base/IPropertyConverter.java new file mode 100644 index 00000000..42ece806 --- /dev/null +++ b/jfoenix/src/main/java/com/jfoenix/bindings/base/IPropertyConverter.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.bindings.base; + +/** + * Converts between two properties (e.g String->Integer and vice versa) + * used when binding properties of different types + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2020-04-30 + */ +public interface IPropertyConverter { + B to(A a); + + A from(B b); +} diff --git a/jfoenix/src/main/java/com/jfoenix/controls/pannable/PannablePane.java b/jfoenix/src/main/java/com/jfoenix/controls/pannable/PannablePane.java new file mode 100644 index 00000000..53b75081 --- /dev/null +++ b/jfoenix/src/main/java/com/jfoenix/controls/pannable/PannablePane.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls.pannable; + +import com.jfoenix.controls.pannable.base.IPannablePane; +import com.jfoenix.controls.pannable.gestures.PanningGestures; +import javafx.beans.property.DoubleProperty; +import javafx.scene.Node; +import javafx.scene.layout.Pane; +import javafx.scene.transform.Scale; + +/** + * Simple pannable pane implementation + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2020-04-30 + */ +public class PannablePane extends Pane implements IPannablePane { + + private Scale scale = new Scale(1, 1, 0, 0); + + public PannablePane() { + getTransforms().add(scale); + scale.yProperty().bind(scale.xProperty()); + } + + @Override + public double getScale() { + return scale.getX(); + } + + @Override + public void setScale(double scale) { + this.scale.setX(scale); + } + + @Override + public DoubleProperty scaleProperty() { + return scale.xProperty(); + } + + public static PannablePane wrap(Node... children) { + PannablePane canvas = new PannablePane(); + PanningGestures.attachViewPortGestures(canvas); + canvas.getChildren().setAll(children); + return canvas; + } +} + diff --git a/jfoenix/src/main/java/com/jfoenix/controls/pannable/PannableScrollPane.java b/jfoenix/src/main/java/com/jfoenix/controls/pannable/PannableScrollPane.java new file mode 100644 index 00000000..49019f44 --- /dev/null +++ b/jfoenix/src/main/java/com/jfoenix/controls/pannable/PannableScrollPane.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls.pannable; + +import com.jfoenix.bindings.CustomBidirectionalBinding; +import com.jfoenix.bindings.base.IPropertyConverter; +import com.jfoenix.controls.pannable.base.IPannablePane; +import javafx.beans.binding.DoubleExpression; +import javafx.beans.property.Property; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.scene.control.ScrollBar; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; + +import java.util.function.Function; + +/** + * Used to add scroll functionality to {@link PannablePane} + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2020-04-30 + */ +public class PannableScrollPane extends Pane { + + private ScrollBar vBar = new ScrollBar(); + private ScrollBar hBar = new ScrollBar(); + + private void init() { + getStyleClass().add("pannable-scroll-pane"); + getChildren().addAll(vBar, hBar); + vBar.setManaged(false); + vBar.setOrientation(Orientation.VERTICAL); + hBar.setManaged(false); + hBar.setOrientation(Orientation.HORIZONTAL); + } + + private static final double SCROLL_PAD = 20; + + public

PannableScrollPane(P pane) { + super(pane); + init(); + bindScrollBar(vBar, pane, pane.translateYProperty(), (p) -> p.heightProperty()); + bindScrollBar(hBar, pane, pane.translateXProperty(), (p) -> p.widthProperty()); + } + +

void bindScrollBar(ScrollBar bar, P pane, Property trans, Function propFun) { + CustomBidirectionalBinding binding = new CustomBidirectionalBinding<>( + trans + , bar.valueProperty() + , new IPropertyConverter() { + @Override + public Number to(Number number) { + return number.doubleValue() * -1; + } + + @Override + public Number from(Number number) { + return number.doubleValue() * -1; + } + }); + binding.bindBi(); + bar.minProperty().bind(pane.scaleProperty().negate()); + bar.maxProperty().bind(propFun.apply(pane).add(SCROLL_PAD).multiply(pane.scaleProperty()).subtract(propFun.apply(this))); + bar.visibleProperty().bind(bar.maxProperty().greaterThan(0)); + } + + public PannableScrollPane() { + init(); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + double w = getWidth(); + double h = getHeight(); + Insets insets = getInsets(); + final double prefWidth = vBar.prefWidth(-1); + vBar.resizeRelocate(w - prefWidth - insets.getRight(), insets.getTop(), prefWidth, h - insets.getTop() - insets.getBottom()); + + final double prefHeight = hBar.prefHeight(-1); + hBar.resizeRelocate(insets.getLeft(), h - prefHeight - insets.getBottom(), w - insets.getLeft() - insets.getRight(), prefHeight); + } +} diff --git a/jfoenix/src/main/java/com/jfoenix/controls/pannable/base/IObservableObject.java b/jfoenix/src/main/java/com/jfoenix/controls/pannable/base/IObservableObject.java new file mode 100644 index 00000000..8124bd80 --- /dev/null +++ b/jfoenix/src/main/java/com/jfoenix/controls/pannable/base/IObservableObject.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls.pannable.base; + +/** + * Observable object is an interface for listening to object events + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2020-04-30 + */ +public interface IObservableObject { + boolean addListener(L listener); + + boolean removeListener(L listener); +} diff --git a/jfoenix/src/main/java/com/jfoenix/controls/pannable/base/IPannablePane.java b/jfoenix/src/main/java/com/jfoenix/controls/pannable/base/IPannablePane.java new file mode 100644 index 00000000..c613aab6 --- /dev/null +++ b/jfoenix/src/main/java/com/jfoenix/controls/pannable/base/IPannablePane.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls.pannable.base; + +import javafx.beans.property.DoubleProperty; + +/** + * Base interface for pannable panes + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2020-04-30 + */ +public interface IPannablePane { + + double getScale(); + + void setScale(double scale); + + DoubleProperty scaleProperty(); +} diff --git a/jfoenix/src/main/java/com/jfoenix/controls/pannable/base/ResilientObservable.java b/jfoenix/src/main/java/com/jfoenix/controls/pannable/base/ResilientObservable.java new file mode 100644 index 00000000..a9a7ad7a --- /dev/null +++ b/jfoenix/src/main/java/com/jfoenix/controls/pannable/base/ResilientObservable.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls.pannable.base; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Implementation of observer pattern using weak references + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2020-04-30 + */ +public class ResilientObservable implements IObservableObject { + + private CopyOnWriteArrayList> listeners = new CopyOnWriteArrayList<>(); + + public ResilientObservable() { + + } + + public boolean addListener(L observer) { + return listeners.add(new WeakObject<>(observer)); + } + + public boolean removeListener(L observer) { + return listeners.remove(new WeakObject<>(observer)); + } + + public void fireEvent(Consumer listenerConsumer) { + ArrayList> toBeRemoved = new ArrayList<>(); + for (Iterator> itr = listeners.iterator(); itr.hasNext(); ) { + WeakObject ref = itr.next(); + try { + // notify + L listener = ref.get(); + if (listener != null) { + listenerConsumer.accept(listener); + } else { + toBeRemoved.add(ref); + } + } catch (RuntimeException e) { + e.printStackTrace(); + toBeRemoved.add(ref); + } + } + // remove null / invalid references + listeners.removeAll(toBeRemoved); + } + + public Collection listeners() { + return Collections.unmodifiableCollection( + listeners.stream().map(Reference::get).collect(Collectors.toList())); + } + + private static class WeakObject extends WeakReference { + private WeakObject(T referent) { + super(referent); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof WeakObject)) { + return false; + } + return ((WeakObject) obj).get() == this.get(); + } + } +} diff --git a/jfoenix/src/main/java/com/jfoenix/controls/pannable/gestures/PanningGestures.java b/jfoenix/src/main/java/com/jfoenix/controls/pannable/gestures/PanningGestures.java new file mode 100644 index 00000000..4e229536 --- /dev/null +++ b/jfoenix/src/main/java/com/jfoenix/controls/pannable/gestures/PanningGestures.java @@ -0,0 +1,261 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls.pannable.gestures; + +import com.jfoenix.controls.pannable.base.IPannablePane; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.event.EventHandler; +import javafx.geometry.Bounds; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.ScrollEvent; + +/** + * Add panning gestures to {@link IPannablePane} region + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2020-04-30 + */ +public class PanningGestures { + + /** + * Drag context used to store mouse drag data + */ + private static class DragContext { + double mouseAnchorX; + double mouseAnchorY; + double translateAnchorX; + double translateAnchorY; + } + + private T canvas; + private final DragContext sceneDragContext = new DragContext(); + + public PanningGestures(T canvas) { + this.canvas = canvas; + } + + /*************************************************************************** + * * + * Panning control properties * + * * + **************************************************************************/ + + private final DoubleProperty minScaleProperty = new SimpleDoubleProperty(0.1d); + + public double getMinScale() { + return minScaleProperty.get(); + } + + public void setMinScale(double minScale) { + minScaleProperty.set(minScale); + } + + public DoubleProperty minScaleProperty() { + return minScaleProperty; + } + + private final DoubleProperty maxScaleProperty = new SimpleDoubleProperty(10.0d); + + public double getMaxScale() { + return maxScaleProperty.get(); + } + + public DoubleProperty maxScaleProperty() { + return maxScaleProperty; + } + + public void setMaxScale(double maxScale) { + maxScaleProperty.set(maxScale); + } + + private final DoubleProperty zoomSpeedProperty = new SimpleDoubleProperty(1.2d); + + public double getZoomSpeed() { + return zoomSpeedProperty.get(); + } + + public DoubleProperty zoomSpeedProperty() { + return zoomSpeedProperty; + } + + public void setZoomSpeed(double zoomSpeed) { + zoomSpeedProperty.set(zoomSpeed); + } + + private SimpleBooleanProperty useViewportGestures = null; + + public final BooleanProperty useViewPortGesturesProperty() { + return useViewportGestures; + } + + // set true to bind panning to canvas size + private boolean bound = false; + + public boolean isBound() { + return bound; + } + + public void setBound(boolean bound) { + this.bound = bound; + } + + /*************************************************************************** + * * + * Panning listeners * + * * + **************************************************************************/ + + private final EventHandler onMousePressedEventHandler = event -> { + // right mouse button => panning + if (!event.isSecondaryButtonDown()) { + return; + } + sceneDragContext.mouseAnchorX = event.getSceneX(); + sceneDragContext.mouseAnchorY = event.getSceneY(); + sceneDragContext.translateAnchorX = canvas.getTranslateX(); + sceneDragContext.translateAnchorY = canvas.getTranslateY(); + }; + + private final EventHandler onMouseDraggedEventHandler = event -> { + // right mouse button => panning + if (!event.isSecondaryButtonDown()) { + return; + } + final Bounds parentLayoutBounds = canvas.getParent().getLayoutBounds(); + final Bounds boundsInParent = canvas.getBoundsInParent(); + // compute new trans X + double newTransX = sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX; + canvas.setTranslateX(bound(newTransX, parentLayoutBounds.getWidth(), boundsInParent.getWidth())); + // compute new trans Y + double newTransY = sceneDragContext.translateAnchorY + event.getSceneY() - sceneDragContext.mouseAnchorY; + canvas.setTranslateY(bound(newTransY, parentLayoutBounds.getHeight(), boundsInParent.getHeight())); + event.consume(); + }; + + + private double bound(double newTrans, double parentSize, double canvasSize) { + if (!bound) { + return newTrans; + } + double lowerBound = parentSize - canvasSize - 20 * canvas.getScale(); + if (parentSize > canvasSize) { + newTrans = 1; + } else { + newTrans = inBound(lowerBound, 1, newTrans); + } + return newTrans; + } + + private double inBound(double lowerBound, double upperBound, double val) { + if (val < lowerBound) { + val = lowerBound; + } else if (val > upperBound) { + val = upperBound; + } + return val; + } + + /** + * Zoom handler: responsible for zooming to a pivot coordinate + */ + private final EventHandler onScrollEventHandler = event -> { + double scale = canvas.getScale(); // currently we only use Y, same value is used for X + if (event.getDeltaY() < 0) { + scale /= getZoomSpeed(); + } else { + scale *= getZoomSpeed(); + } + scale = clamp(scale, minScaleProperty.get(), maxScaleProperty.get()); + Point2D currentPoint = canvas.parentToLocal(event.getX(), event.getY()); + canvas.setScale(scale); + Point2D newPoint = canvas.localToParent(currentPoint); + + final Bounds parentLayoutBounds = canvas.getParent().getLayoutBounds(); + final Bounds boundsInParent = canvas.getBoundsInParent(); + double lowerBound = parentLayoutBounds.getWidth() - boundsInParent.getWidth() - 20; + canvas.setTranslateX(bound(canvas.getTranslateX() - (newPoint.getX() - event.getX()), + parentLayoutBounds.getWidth(), boundsInParent.getWidth())); + canvas.setTranslateY(bound(canvas.getTranslateY() - (newPoint.getY() - event.getY()), + parentLayoutBounds.getHeight(), boundsInParent.getHeight())); + event.consume(); + }; + + public static double clamp(double value, double min, double max) { + if (Double.compare(value, min) < 0) { + return min; + } + if (Double.compare(value, max) > 0) { + return max; + } + return value; + } + + /*************************************************************************** + * * + * Attach methods * + * * + **************************************************************************/ + + public static PanningGestures attachViewPortGestures(T pannableCanvas) { + return attachViewPortGestures(pannableCanvas, false); + } + + public static PanningGestures attachViewPortGestures(T pannableCanvas, boolean configurable) { + PanningGestures panningGestures = new PanningGestures<>(pannableCanvas); + if (configurable) { + panningGestures.useViewportGestures = new SimpleBooleanProperty(true); + panningGestures.useViewportGestures.addListener((o, oldVal, newVal) -> { + final Parent parent = pannableCanvas.parentProperty().get(); + if (parent == null) { + return; + } + if (newVal) { + parent.addEventHandler(MouseEvent.MOUSE_PRESSED, panningGestures.onMousePressedEventHandler); + parent.addEventHandler(MouseEvent.MOUSE_DRAGGED, panningGestures.onMouseDraggedEventHandler); + parent.addEventHandler(ScrollEvent.ANY, panningGestures.onScrollEventHandler); + } else { + parent.removeEventHandler(MouseEvent.MOUSE_PRESSED, panningGestures.onMousePressedEventHandler); + parent.removeEventHandler(MouseEvent.MOUSE_DRAGGED, panningGestures.onMouseDraggedEventHandler); + parent.removeEventHandler(ScrollEvent.ANY, panningGestures.onScrollEventHandler); + } + }); + } + pannableCanvas.parentProperty().addListener((o, oldVal, newVal) -> { + if (oldVal != null) { + oldVal.removeEventHandler(MouseEvent.MOUSE_PRESSED, panningGestures.onMousePressedEventHandler); + oldVal.removeEventHandler(MouseEvent.MOUSE_DRAGGED, panningGestures.onMouseDraggedEventHandler); + oldVal.removeEventHandler(ScrollEvent.ANY, panningGestures.onScrollEventHandler); + } + if (newVal != null) { + newVal.addEventHandler(MouseEvent.MOUSE_PRESSED, panningGestures.onMousePressedEventHandler); + newVal.addEventHandler(MouseEvent.MOUSE_DRAGGED, panningGestures.onMouseDraggedEventHandler); + newVal.addEventHandler(ScrollEvent.ANY, panningGestures.onScrollEventHandler); + } + }); + return panningGestures; + } +}