package com.faforever.client.notification; import com.faforever.client.notification.Action.ActionCallback; import com.faforever.client.preferences.PreferencesService; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.value.ChangeListener; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; import javafx.util.Duration; import javax.annotation.PostConstruct; import javax.annotation.Resource; import static javafx.util.Duration.millis; public class TransientNotificationController { @FXML Pane transientNotificationRoot; @FXML Label messageLabel; @FXML Label titleLabel; @FXML ImageView imageView; @Resource PreferencesService preferencesService; private ChangeListener<Number> animationListener; private ActionCallback action; private Timeline timeline; private int toastDisplayTime; @FXML void initialize() { Rectangle rectangle = new Rectangle(); rectangle.widthProperty().bind(transientNotificationRoot.widthProperty()); rectangle.heightProperty().bind(transientNotificationRoot.heightProperty()); // Wait until the height is known, then animate it this.animationListener = (observable, oldValue, newValue) -> { if (newValue != null) { observable.removeListener(animationListener); transientNotificationRoot.setMaxHeight(0); transientNotificationRoot.setVisible(true); TransientNotificationController.this.animate(newValue); } }; transientNotificationRoot.setVisible(false); transientNotificationRoot.setClip(rectangle); transientNotificationRoot.heightProperty().addListener(animationListener); } private void animate(Number height) { timeline = new Timeline(); timeline.setAutoReverse(true); timeline.setCycleCount(2); timeline.getKeyFrames().addAll( new KeyFrame(millis(300), new KeyValue(transientNotificationRoot.maxHeightProperty(), height, Interpolator.LINEAR)), new KeyFrame(millis(300 + toastDisplayTime), new KeyValue(transientNotificationRoot.maxHeightProperty(), height)) ); timeline.setOnFinished(event -> dismiss()); timeline.playFromStart(); } private void dismiss() { timeline.stop(); Pane parent = (Pane) transientNotificationRoot.getParent(); if (parent == null) { return; } parent.getChildren().remove(transientNotificationRoot); } @PostConstruct void postConstruct() { // Divided by two because it's used in a cycle toastDisplayTime = preferencesService.getPreferences().getNotification().getToastDisplayTime() / 2; transientNotificationRoot.setOnMouseEntered(event -> timeline.pause()); transientNotificationRoot.setOnMouseExited(event -> timeline.playFrom(Duration.millis(300 + toastDisplayTime))); } public void setNotification(TransientNotification notification) { titleLabel.setText(notification.getTitle()); messageLabel.setText(notification.getText()); imageView.setImage(notification.getImage()); action = notification.getActionCallback(); } @FXML void onCloseButtonClicked() { dismiss(); } public Region getRoot() { return transientNotificationRoot; } @FXML public void onClicked(MouseEvent event) { action.call(event); } }