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

com.jfoenix.controls.JFXSnackbar Maven / Gradle / Ivy

There is a newer version: 9.0.10
Show newest version
/*
 * 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;

import com.jfoenix.controls.JFXButton.ButtonType;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @see  Snackbars & toasts
 * 

* The use of a javafx Popup or PopupContainer for notifications would seem intuitive but Popups are displayed in their * own dedicated windows and alligning the popup window and handling window on top layering is more trouble then it is * worth. */ public class JFXSnackbar extends StackPane { private Label toast; private JFXButton action; private Pane snackbarContainer; private Group popup; private ChangeListener sizeListener; private AtomicBoolean processingQueue = new AtomicBoolean(false); private ConcurrentLinkedQueue eventQueue = new ConcurrentLinkedQueue<>(); private StackPane actionContainer; Interpolator easeInterpolator = Interpolator.SPLINE(0.250, 0.100, 0.250, 1.000); public JFXSnackbar() { this(null); } public JFXSnackbar(Pane snackbarContainer) { BorderPane bPane = new BorderPane(); toast = new Label(); toast.setMinWidth(Control.USE_PREF_SIZE); toast.getStyleClass().add("jfx-snackbar-toast"); toast.setWrapText(true); StackPane toastContainer = new StackPane(toast); toastContainer.setPadding(new Insets(20)); bPane.setLeft(toastContainer); action = new JFXButton(); action.setMinWidth(Control.USE_PREF_SIZE); action.setButtonType(ButtonType.FLAT); action.getStyleClass().add("jfx-snackbar-action"); // actions will be added upon showing the snackbar if needed actionContainer = new StackPane(action); actionContainer.setPadding(new Insets(0, 10, 0, 0)); bPane.setRight(actionContainer); toast.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> { if (this.getPrefWidth() == -1) { return this.getPrefWidth(); } double actionWidth = actionContainer.isVisible() ? actionContainer.getWidth() : 0.0; return this.prefWidthProperty().get() - actionWidth; }, this.prefWidthProperty(), actionContainer.widthProperty(), actionContainer.visibleProperty())); //bind the content's height and width from this snackbar allowing the content's dimensions to be set externally bPane.prefWidthProperty().bind(this.prefWidthProperty()); final BorderPane content = bPane; content.getStyleClass().add("jfx-snackbar-content"); //setting a shadow enlarges the snackbar height leaving a gap below it //JFXDepthManager.setDepth(content, 4); //wrap the content in a group so that the content is managed inside its own container //but the group is not managed in the snackbarContainer so it does not affect any layout calculations popup = new Group(); popup.getChildren().add(content); popup.setManaged(false); popup.setVisible(false); popup.idProperty().bind(this.idProperty()); this.getStyleClass().addListener((ListChangeListener) c -> { while (c.next()) { if (c.wasAdded()) { popup.getStyleClass().addAll(c.getAddedSubList()); } if (c.wasRemoved()) { popup.getStyleClass().removeAll(c.getRemoved()); } } }); sizeListener = (o, oldVal, newVal) -> { refreshPopup(); }; // register the container before resizing it registerSnackbarContainer(snackbarContainer); // resize the popup if its layout has been changed popup.layoutBoundsProperty().addListener((o, oldVal, newVal) -> refreshPopup()); addEventHandler(SnackbarEvent.SNACKBAR, e -> enqueue(e)); //This control actually orchestrates the popup logic and is never visibly displayed. this.setVisible(false); this.setManaged(false); } /*************************************************************************** * * Setters / Getters * * **************************************************************************/ public Pane getPopupContainer() { return snackbarContainer; } /*************************************************************************** * * Public API * * **************************************************************************/ public void registerSnackbarContainer(Pane snackbarContainer) { if (snackbarContainer != null) { if (this.snackbarContainer != null) { //since listeners are added the container should be properly registered/unregistered throw new IllegalArgumentException("Snackbar Container already set"); } this.snackbarContainer = snackbarContainer; this.snackbarContainer.getChildren().add(popup); this.snackbarContainer.heightProperty().addListener(sizeListener); this.snackbarContainer.widthProperty().addListener(sizeListener); } } public void unregisterSnackbarContainer(Pane snackbarContainer) { if (snackbarContainer != null) { if (this.snackbarContainer == null) { throw new IllegalArgumentException("Snackbar Container not set"); } this.snackbarContainer.getChildren().remove(popup); this.snackbarContainer.heightProperty().removeListener(sizeListener); this.snackbarContainer.widthProperty().removeListener(sizeListener); this.snackbarContainer = null; } } public void show(String toastMessage, long timeout) { this.show(toastMessage, null, timeout, null); } public void show(String message, String actionText, EventHandler actionHandler) { this.show(message, actionText, -1, actionHandler); } public void show(String message, String actionText, long timeout, EventHandler actionHandler) { toast.setText(message); if (actionText != null && !actionText.isEmpty()) { action.setVisible(true); actionContainer.setVisible(true); actionContainer.setManaged(true); // to force updating the layout bounds action.setText(""); action.setText(actionText); action.setOnMouseClicked(actionHandler); } else { actionContainer.setVisible(false); actionContainer.setManaged(false); action.setVisible(false); } Timeline animation = getTimeline(timeout); animation.play(); } private Timeline getTimeline(long timeout) { Timeline animation; if (timeout <= 0) { animation = new Timeline( new KeyFrame( Duration.ZERO, e -> popup.toBack(), new KeyValue(popup.visibleProperty(), false, Interpolator.EASE_BOTH), new KeyValue(popup.translateYProperty(), popup.getLayoutBounds().getHeight(), easeInterpolator), new KeyValue(popup.opacityProperty(), 0, easeInterpolator) ), new KeyFrame( Duration.millis(10), e -> popup.toFront(), new KeyValue(popup.visibleProperty(), true, Interpolator.EASE_BOTH) ), new KeyFrame(Duration.millis(300), new KeyValue(popup.opacityProperty(), 1, easeInterpolator), new KeyValue(popup.translateYProperty(), 0, easeInterpolator) ) ); animation.setCycleCount(1); } else { animation = new Timeline( new KeyFrame( Duration.ZERO, (e) -> popup.toBack(), new KeyValue(popup.visibleProperty(), false, Interpolator.EASE_BOTH), new KeyValue(popup.translateYProperty(), popup.getLayoutBounds().getHeight(), easeInterpolator), new KeyValue(popup.opacityProperty(), 0, easeInterpolator) ), new KeyFrame( Duration.millis(10), (e) -> popup.toFront(), new KeyValue(popup.visibleProperty(), true, Interpolator.EASE_BOTH) ), new KeyFrame(Duration.millis(300), new KeyValue(popup.opacityProperty(), 1, easeInterpolator), new KeyValue(popup.translateYProperty(), 0, easeInterpolator) ), new KeyFrame(Duration.millis(timeout / 2)) ); animation.setAutoReverse(true); animation.setCycleCount(2); animation.setOnFinished((e) -> processSnackbars()); } return animation; } public void close() { Timeline animation = new Timeline( new KeyFrame( Duration.ZERO, e -> popup.toFront(), new KeyValue(popup.opacityProperty(), 1, easeInterpolator), new KeyValue(popup.translateYProperty(), 0, easeInterpolator) ), new KeyFrame( Duration.millis(290), new KeyValue(popup.visibleProperty(), true, Interpolator.EASE_BOTH) ), new KeyFrame(Duration.millis(300), e -> popup.toBack(), new KeyValue(popup.visibleProperty(), false, Interpolator.EASE_BOTH), new KeyValue(popup.translateYProperty(), popup.getLayoutBounds().getHeight(), easeInterpolator), new KeyValue(popup.opacityProperty(), 0, easeInterpolator) ) ); animation.setCycleCount(1); animation.setOnFinished(e -> processSnackbars()); animation.play(); } private void processSnackbars() { SnackbarEvent qevent = eventQueue.poll(); if (qevent != null) { if (qevent.isPersistent()) { show(qevent.getMessage(), qevent.getActionText(), qevent.getActionHandler()); } else { show(qevent.getMessage(), qevent.getActionText(), qevent.getTimeout(), qevent.getActionHandler()); } } else { //The enqueue method and this listener should be executed sequentially on the FX Thread so there //should not be a race condition processingQueue.getAndSet(false); } } public void refreshPopup() { Bounds contentBound = popup.getLayoutBounds(); double offsetX = Math.ceil(snackbarContainer.getWidth() / 2) - Math.ceil(contentBound.getWidth() / 2); double offsetY = snackbarContainer.getHeight() - contentBound.getHeight(); popup.setLayoutX(offsetX); popup.setLayoutY(offsetY); } public void enqueue(SnackbarEvent event) { eventQueue.add(event); if (processingQueue.compareAndSet(false, true)) { Platform.runLater(() -> { SnackbarEvent qevent = eventQueue.poll(); if (qevent != null) { if (qevent.isPersistent()) { show(qevent.getMessage(), qevent.getActionText(), qevent.getActionHandler()); } else { show(qevent.getMessage(), qevent.getActionText(), qevent.getTimeout(), qevent.getActionHandler()); } } }); } } /*************************************************************************** * * Event API * * **************************************************************************/ public static class SnackbarEvent extends Event { public static final EventType SNACKBAR = new EventType<>(Event.ANY, "SNACKBAR"); private final String message; private final String actionText; private final long timeout; private final boolean persistent; private final EventHandler actionHandler; public SnackbarEvent(String message) { this(message, null, 3000, false, null); } public SnackbarEvent(String message, String actionText, long timeout, boolean persistent, EventHandler actionHandler) { super(SNACKBAR); this.message = message; this.actionText = actionText; this.timeout = timeout < 1 ? 3000 : timeout; this.actionHandler = actionHandler; this.persistent = persistent; } public String getMessage() { return message; } public String getActionText() { return actionText; } public long getTimeout() { return timeout; } public EventHandler getActionHandler() { return actionHandler; } @Override public EventType getEventType() { return (EventType) super.getEventType(); } public boolean isPersistent() { return persistent; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy