All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sshtools.jajafx.Flinger Maven / Gradle / Ivy

The newest version!
package com.sshtools.jajafx;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import javafx.animation.TranslateTransition;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;

public class Flinger extends StackPane {

	public enum Direction {
		VERTICAL, HORIZONTAL
	}

	private Pane container;
	private TranslateTransition slideTransition;
	private Rectangle slideClip;

	private Property directionProperty = new SimpleObjectProperty(Direction.HORIZONTAL);
	private BooleanProperty leftOrUpDisabledProperty = new SimpleBooleanProperty();
	private BooleanProperty rightOrDownDisabledProperty = new SimpleBooleanProperty();
	private DoubleProperty gapProperty = new SimpleDoubleProperty();
	private boolean dragged;
	private boolean wasDragged;

	public Flinger() {

		setup();

		directionProperty.addListener(new ChangeListener() {

			@Override
			public void changed(ObservableValue observable, Direction oldValue,
					Direction newValue) {
				setup();
			}
		});

		// Handle events
		final AtomicReference deltaEvent = new AtomicReference();
		setOnScroll(scr -> {
			if (scr.getDeltaY() < 0) {
				slideRightOrDown();
			} else {
				slideLeftOrUp();
			}
		});
		setOnMousePressed(eh -> {
			wasDragged = false;
			dragged = false;
			deltaEvent.set(eh);
		});
		setOnMouseReleased(eh -> {
			if (!dragged) {
				/* Clicked. For the the root child item */
				EventTarget n = eh.getTarget();
				int depth = 0;
				do {
					if(n instanceof Node && container.equals(((Node)n).getParent())) {
						if(depth > 0) {
							if(!onAction.isNull().get()) {
								eh.consume();
								onAction.get().handle(new ActionEvent(this, n));
							}
						}
						break;
					}
					else if(n instanceof Node) {
						n = ((Node)n).getParent();
						depth++;
					}
					else
						break;
				}
				while(n != null);
			}
			else {
				eh.consume();
			}
			dragged = false;
		});

		// Handle drag events, will only allow drag until the first or last item
		// is revealed
		setOnMouseDragged(event -> {
			wasDragged = true;
			var mouseEvent = deltaEvent.get();
			if (mouseEvent != null && directionProperty.getValue().equals(Direction.HORIZONTAL)) {
				var delta = event.getX() - mouseEvent.getX();
				var newX = container.getTranslateX() + delta;
				var containerWidth = container.prefWidth(getHeight());
				if (newX + containerWidth < getWidth() - 1) {
					newX = container.getTranslateX();
				} else if (newX > 0) {
					newX = 0;
				}
				if (newX != container.getTranslateX()) {
					dragged = true;
					container.setTranslateX(newX);
				}
			}

			if (mouseEvent != null && directionProperty.getValue().equals(Direction.VERTICAL)) {
				var delta = event.getY() - mouseEvent.getY();
				var newX = container.getTranslateY() + delta;
				var containerHeight = container.prefHeight(getWidth());
				if (newX + containerHeight < getHeight()) {
					newX = container.getTranslateY();
				} else if (newX > 0) {
					newX = 0;
				}

				if (newX != container.getTranslateY()) {
					dragged = true;
					container.setTranslateY(newX);
				}
			}

			setAvailable();
			deltaEvent.set(event);
		});

	}

	public final boolean isWasDragged() {
		return wasDragged;
	}

	public final ObjectProperty> onActionProperty() {
		return onAction;
	}

	public final void setOnAction(EventHandler value) {
		onActionProperty().set(value);
	}

	public final EventHandler getOnAction() {
		return onActionProperty().get();
	}

	private ObjectProperty> onAction = new ObjectPropertyBase>() {
		@Override
		protected void invalidated() {
			setEventHandler(ActionEvent.ACTION, get());
		}

		@Override
		public Object getBean() {
			return Flinger.this;
		}

		@Override
		public String getName() {
			return "onAction";
		}
	};

	public ReadOnlyBooleanProperty leftOrUpDisableProperty() {
		return leftOrUpDisabledProperty;
	}

	public ReadOnlyBooleanProperty rightOrDownDisableProperty() {
		return rightOrDownDisabledProperty;
	}

	public Property directionProperty() {
		return directionProperty;
	}

	public DoubleProperty gapProperty() {
		return gapProperty;
	}

	public Pane getContent() {
		return container;
	}

	public void slideLeftOrUp() {
		/*
		 * We should only get this action if the button is enabled, which means at least
		 * one button is partially obscured on the left
		 */

		double scroll = 0;

		/*
		 * The position of the child within the container. When we find a node that
		 * crosses '0', that is how much this single scroll will adjust by, so
		 * completely revealing the hidden side
		 */

		if (directionProperty.getValue().equals(Direction.VERTICAL)) {

			for (var n : container.getChildren()) {
				double p = n.getLayoutY() + container.getTranslateY();

				double amt = p + n.getLayoutBounds().getHeight();
				if (amt >= 0) {
					scroll = Math.abs(n.getLayoutBounds().getHeight() - amt + gapProperty.get());
					if (container.getTranslateY() + scroll > 0) {
						scroll = 0;
						break;
					} else if (scroll > 0)
						break;
				}
			}
		} else {
			for (var n : container.getChildren()) {
				double p = n.getLayoutX() + container.getTranslateX();
				double amt = p + n.getLayoutBounds().getWidth();
				if (amt >= 0) {
					scroll = Math.abs(n.getLayoutBounds().getWidth() - amt);
					if (container.getTranslateX() + scroll > 0) {
						scroll = 0;
						break;
					} else if (scroll > 0)
						break;
				}
			}
		}

		setAvailable();
		if (scroll > 0) {
			if (directionProperty.getValue().equals(Direction.VERTICAL)) {
				slideTransition.setFromY(container.getTranslateY());
				slideTransition.setToY(container.getTranslateY() + scroll);
			} else {
				slideTransition.setFromX(container.getTranslateX());
				slideTransition.setToX(container.getTranslateX() + scroll);
			}
			slideTransition.setOnFinished(ae -> {
				setAvailable();
			});
			slideTransition.play();
		}
	}

	public void slideRightOrDown() {
		/*
		 * We should only get this action if the button is enabled, which means at least
		 * one button is partially obscured on the left
		 */

		var scroll = 0d;
		var c = container.getChildren();

		/*
		 * Search backwards through the nodes until on whose X or Y position is is in
		 * the visible portion
		 */
		if (directionProperty.getValue().equals(Direction.VERTICAL)) {

			for (int i = c.size() - 1; i >= 0; i--) {
				Node n = c.get(i);
				double p = n.getLayoutY() + container.getTranslateY();
				if (p <= getHeight()) {
					scroll = n.getLayoutBounds().getHeight() + gapProperty.get() - (getHeight() - p);
					break;
				}
			}
		} else {

			for (int i = c.size() - 1; i >= 0; i--) {
				Node n = c.get(i);
				double p = n.getLayoutX() + container.getTranslateX();
				if (p < getWidth()) {
					scroll = (getWidth() - (p + n.getLayoutBounds().getWidth() + 1 + gapProperty.get())) * -1;
					break;
				}
			}

		}
		setAvailable();
		if (scroll > 0) {
			leftOrUpDisabledProperty.set(false);
			if (directionProperty.getValue().equals(Direction.VERTICAL)) {
				slideTransition.setFromY(container.getTranslateY());
				slideTransition.setToY(container.getTranslateY() - scroll);
			} else {
				slideTransition.setFromX(container.getTranslateX());
				slideTransition.setToX(container.getTranslateX() - scroll);
			}
			slideTransition.setOnFinished(ae -> {
				setAvailable();
			});
			slideTransition.play();
		}
	}

	public void recentre() {
		setAvailable();

		var centre = getLaunchBarOffset();

		if (directionProperty.getValue().equals(Direction.VERTICAL)) {
			slideTransition.setFromY(container.getTranslateY());
			slideTransition.setToY(centre);
		} else {
			slideTransition.setFromX(container.getTranslateX());
			slideTransition.setToX(centre);
		}
		slideTransition.setOnFinished(eh -> setAvailable());
		slideTransition.stop();
		slideTransition.play();
	}

	@Override
	protected void layoutChildren() {
		for (var node : getChildren()) {
			layoutInArea(node, 0, 0, getWidth(), getHeight(), 0, HPos.LEFT, VPos.TOP);
		}
	}

	private double getLaunchBarOffset() {
		return directionProperty.getValue().equals(Direction.VERTICAL)
				? (getHeight() - container.prefHeight(getWidth())) / 2d
				: (getWidth() - container.prefWidth(getHeight())) / 2d;
	}

	private void requiredSpaceChanged() {
		layoutContainer();
	}

	private void availableSpaceChanged() {
		layoutContainer();
	}

	private void layoutContainer() {
		if (directionProperty.getValue().equals(Direction.VERTICAL)) {
			container.setTranslateX((getWidth() - container.prefWidth(getHeight())) / 2d);

		} else {
			container.setTranslateY((getHeight() - container.prefHeight(getWidth())) / 2d);
		}
		layout();
		recentre();
	}

	private void setup() {
		List children = null;
		if (container != null) {
			children = new ArrayList(container.getChildrenUnmodifiable());
			container.getChildren().clear();
		}
		getChildren().clear();

		container = directionProperty.getValue().equals(Direction.VERTICAL) ? new VBox() : new HBox();
		container.setPrefSize(USE_COMPUTED_SIZE, USE_COMPUTED_SIZE);

		if (directionProperty.getValue().equals(Direction.VERTICAL)) {
			((VBox) container).spacingProperty().bind(gapProperty);
		} else {
			((HBox) container).spacingProperty().bind(gapProperty);
		}

		widthProperty().addListener(new ChangeListener() {

			@Override
			public void changed(ObservableValue observable, Number oldValue, Number newValue) {
				availableSpaceChanged();
			}
		});
		heightProperty().addListener(new ChangeListener() {

			@Override
			public void changed(ObservableValue observable, Number oldValue, Number newValue) {
				availableSpaceChanged();
			}
		});

		/*
		 * Watch for new children being added, and so changing the amount of space
		 * required
		 */
		container.getChildren().addListener(new ListChangeListener() {

			@Override
			public void onChanged(ListChangeListener.Change c) {
				requiredSpaceChanged();
			}
		});

		// Do not want the container managed, we lay it out ourselves
		container.setManaged(false);

		// Clip the content container to the width of this container
		slideClip = new Rectangle();
		slideClip.widthProperty().bind(widthProperty());
		slideClip.heightProperty().bind(heightProperty());
		setClip(slideClip);

		// Transition for sliding
		slideTransition = new TranslateTransition(Duration.seconds(0.125), container);
		slideTransition.setAutoReverse(false);
		slideTransition.setCycleCount(1);

		// Add back any children
		if (children != null) {
			container.getChildren().addAll(children);
		}

		// Add the new container to the scene
		getChildren().add(container);
	}

	private void setAvailable() {
		var scroll = 0d;
		var c = container.getChildren();
		if (directionProperty.getValue().equals(Direction.HORIZONTAL)) {
			for (var i = c.size() - 1; i >= 0; i--) {
				var  n = c.get(i);
				var p = n.getLayoutX() + container.getTranslateX();
				if (p < getWidth()) {
					scroll = (getWidth() - (p + n.getLayoutBounds().getWidth() + 1 + gapProperty.get())) * -1;
					break;
				}
			}

			leftOrUpDisabledProperty.set(container.getTranslateX() >= 0);
			rightOrDownDisabledProperty.set(scroll == 0);
		} else {
			for (var i = c.size() - 1; i >= 0; i--) {
				var n = c.get(i);
				var p = n.getLayoutY() + container.getTranslateY();
				if (p <= getHeight()) {
					scroll = n.getLayoutBounds().getHeight() + gapProperty.get() - (getHeight() - p);
					break;
				}
			}
			leftOrUpDisabledProperty.set(container.getTranslateY() >= 0);
			rightOrDownDisabledProperty.set(scroll == 0);
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy