package lighthouse.controls; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableDoubleValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.util.Duration; import lighthouse.protocol.LHUtils; import lighthouse.utils.GuiUtils; import lighthouse.utils.easing.EasingMode; import lighthouse.utils.easing.ElasticInterpolator; import javax.annotation.Nullable; /** * Wraps the given Node in a BorderPane and allows a thin bar to slide in from the bottom or top, squeezing the content * node. The API allows different "items" to be added/removed and they will be displayed one at a time, fading between * them when the topmost is removed. Each item is meant to be used for e.g. a background task and can contain a button * and/or a progress bar. */ public class NotificationBarPane extends BorderPane { public static final Duration ANIM_IN_DURATION = GuiUtils.UI_ANIMATION_TIME.multiply(2); public static final Duration ANIM_OUT_DURATION = GuiUtils.UI_ANIMATION_TIME; private VBox bar; private double barHeight; public class Item { public final StringProperty label; @Nullable public final ObservableDoubleValue progress; protected HBox entry; private Label labelControl; @Nullable private ProgressBar progressBar; public Item(String initialLabel, @Nullable ObservableDoubleValue progress, @Nullable Button button) { this.progress = progress; labelControl = new Label(); labelControl.setText(initialLabel); labelControl.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(labelControl, Priority.ALWAYS); label = labelControl.textProperty(); entry = new HBox(labelControl); if (progress != null) { progressBar = new ProgressBar(); progressBar.setMinWidth(200); progressBar.progressProperty().bind(progress); entry.getChildren().add(progressBar); } if (button != null) { entry.getChildren().add(button); } entry.getStyleClass().add("notification-bar-item"); entry.setFillHeight(true); entry.setAlignment(Pos.CENTER_LEFT); } public void cancel() { items.remove(this); } } public final ObservableList<Item> items; public NotificationBarPane(Node content) { super(content); // Just for sizing Item fakeItem = new Item("infobar!", null, new Button("foo")); bar = new VBox(fakeItem.entry); bar.setMinHeight(0.0); bar.getStyleClass().add("info-bar"); bar.setFillWidth(true); setBottom(bar); // Figure out the height of the bar based on the CSS. Must wait until after we've been added to the parent node. sceneProperty().addListener(o -> { if (getParent() == null) return; getParent().applyCss(); getParent().layout(); barHeight = bar.getHeight(); bar.setPrefHeight(0.0); bar.getChildren().remove(fakeItem.entry); }); items = FXCollections.observableArrayList(); items.addListener(this::processItemChange); } private void processItemChange(ListChangeListener.Change<? extends Item> change) { while (change.next()) { if (change.wasRemoved()) { bar.getChildren().removeAll(LHUtils.mapList(change.getRemoved(), item -> item.entry)); } if (change.wasAdded()) { bar.getChildren().addAll(LHUtils.mapList(change.getAddedSubList(), item -> item.entry)); } } showOrHide(); } private void showOrHide() { if (items.isEmpty()) animateOut(); else animateIn(); } public boolean isShowing() { return bar.getPrefHeight() > 0; } private void animateIn() { animate(barHeight * items.size()); } private void animateOut() { animate(0.0); } private Timeline timeline; protected void animate(Number target) { if (timeline != null) { timeline.stop(); timeline = null; } Duration duration; Interpolator interpolator; if (target.intValue() > 0) { interpolator = new ElasticInterpolator(EasingMode.EASE_OUT, 1, 2); duration = ANIM_IN_DURATION; } else { interpolator = Interpolator.EASE_OUT; duration = ANIM_OUT_DURATION; } KeyFrame kf = new KeyFrame(duration, new KeyValue(bar.prefHeightProperty(), target, interpolator)); timeline = new Timeline(kf); timeline.setOnFinished(x -> timeline = null); timeline.play(); } public Item displayNewItem(String string) { Item item = createItem(string, null, null); items.add(item); return item; } public Item displayNewItem(String string, ObservableDoubleValue progress) { Item item = createItem(string, progress); items.add(item); return item; } public Item displayNewItem(String string, Button btn) { Item item = createItem(string, btn); items.add(item); return item; } public Item createItem(String string, @Nullable ObservableDoubleValue progress) { return new Item(string, progress, null); } public Item createItem(String string, @Nullable Button button) { return new Item(string, null, button); } public Item createItem(String string, @Nullable ObservableDoubleValue progress, @Nullable Button button) { return new Item(string, progress, button); } }