package lighthouse.utils; import com.google.common.util.concurrent.*; import com.sun.prism.*; import com.sun.prism.sw.*; import com.vinumeris.crashfx.*; import javafx.animation.*; import javafx.application.*; import javafx.beans.binding.*; import javafx.beans.property.*; import javafx.beans.value.*; import javafx.fxml.*; import javafx.scene.*; import javafx.scene.control.*; import javafx.scene.effect.*; import javafx.scene.image.*; import javafx.scene.layout.*; import javafx.scene.paint.*; import javafx.scene.shape.*; import javafx.stage.*; import javafx.util.*; import lighthouse.*; import lighthouse.protocol.*; import org.bitcoinj.core.*; import org.controlsfx.control.*; import org.slf4j.*; import javax.annotation.*; import java.io.*; import java.net.*; import java.nio.file.Path; import java.util.*; import java.util.concurrent.*; import java.util.function.*; import static lighthouse.protocol.LHUtils.*; public class GuiUtils { public static final Logger log = LoggerFactory.getLogger(GuiUtils.class); public static void runAlert(BiConsumer<Stage, AlertWindowController> setup) { try { // JavaFX doesn't actually have a standard alert template. Instead the Scene Builder app will create FXML // files for an alert window for you, and then you customise it as you see fit. Stage dialogStage = new Stage(); dialogStage.initModality(Modality.APPLICATION_MODAL); FXMLLoader loader = new FXMLLoader(GuiUtils.class.getResource("alert.fxml"), I18nUtil.translations); Pane pane = loader.load(); AlertWindowController controller = loader.getController(); setup.accept(dialogStage, controller); dialogStage.setScene(new Scene(pane)); dialogStage.showAndWait(); } catch (Throwable e) { // We crashed whilst trying to show the alert dialog. This can happen if we're being crashed by inbound // closures onto the event thread which will execute in the nested event loop. Just give up here: at the // moment we have no way to filter them out of the event queue. e.printStackTrace(); Runtime.getRuntime().exit(1); } } public static void informationalAlert(String message, String details, Object... args) { String formattedDetails = String.format(details, args); Runnable r = () -> runAlert((stage, controller) -> controller.informational(stage, message, formattedDetails)); if (Platform.isFxApplicationThread()) r.run(); else Platform.runLater(r); } public static final int UI_ANIMATION_TIME_MSEC = 500; public static final Duration UI_ANIMATION_TIME = Duration.millis(UI_ANIMATION_TIME_MSEC); public static Animation fadeIn(Node ui) { return fadeIn(ui, 0, 1.0); } public static Animation fadeIn(Node ui, int delayMillis, double targetValue) { ui.setCache(true); ui.setCacheHint(CacheHint.SPEED); FadeTransition ft = new FadeTransition(Duration.millis(UI_ANIMATION_TIME_MSEC), ui); ft.setFromValue(ui.getOpacity()); ft.setToValue(targetValue); ft.setOnFinished(ev -> ui.setCache(false)); ft.setDelay(Duration.millis(delayMillis)); ft.play(); return ft; } public static Animation fadeOut(Node ui) { ui.setCache(true); ui.setCacheHint(CacheHint.SPEED); FadeTransition ft = new FadeTransition(Duration.millis(UI_ANIMATION_TIME_MSEC), ui); ft.setFromValue(ui.getOpacity()); ft.setToValue(0.0); ft.setOnFinished(ev -> ui.setCache(false)); ft.play(); return ft; } public static Animation fadeOutAndRemove(Pane parentPane, Node... nodes) { Animation animation = fadeOut(nodes[0]); animation.setOnFinished(actionEvent -> parentPane.getChildren().removeAll(nodes)); for (int i = 1; i < nodes.length; i++) { fadeOut(nodes[i]); } return animation; } public static Animation fadeOutAndRemove(Duration duration, Pane parentPane, Node... nodes) { nodes[0].setCache(true); FadeTransition ft = new FadeTransition(duration, nodes[0]); ft.setFromValue(nodes[0].getOpacity()); ft.setToValue(0.0); ft.setOnFinished(ev -> parentPane.getChildren().removeAll(nodes)); ft.play(); return ft; } public static void blurOut(Node node) { GaussianBlur blur = new GaussianBlur(0.0); node.setEffect(blur); Timeline timeline = new Timeline(); KeyValue kv = new KeyValue(blur.radiusProperty(), 10.0); KeyFrame kf = new KeyFrame(UI_ANIMATION_TIME, kv); timeline.getKeyFrames().add(kf); timeline.play(); } public static void blurIn(Node node, Duration duration) { GaussianBlur blur = (GaussianBlur) node.getEffect(); if (blur == null) { Main.log.error("BUG: Attempted to cancel non-existent blur."); return; } Timeline timeline = new Timeline(); KeyValue kv = new KeyValue(blur.radiusProperty(), 0.0); KeyFrame kf = new KeyFrame(duration, kv); timeline.getKeyFrames().add(kf); timeline.setOnFinished(actionEvent -> node.setEffect(null)); timeline.play(); } /* public static void blurOut(Node node) { BoxBlur blur = new BoxBlur(); blur.setIterations(1); blur.setWidth(0.0); blur.setHeight(0.0); Timeline timeline = new Timeline(); KeyFrame kf = new KeyFrame(Duration.millis(UI_ANIMATION_TIME_MSEC), new KeyValue(blur.widthProperty(), 8.0), new KeyValue(blur.heightProperty(), 8.0)); timeline.getKeyFrames().add(kf); timeline.play(); node.setEffect(blur); } public static void blurIn(Node node) { BoxBlur blur = (BoxBlur) node.getEffect(); Timeline timeline = new Timeline(); KeyFrame kf = new KeyFrame(Duration.millis(UI_ANIMATION_TIME_MSEC), new KeyValue(blur.widthProperty(), 0.0), new KeyValue(blur.heightProperty(), 0.0)); timeline.getKeyFrames().add(kf); timeline.setOnFinished(actionEvent -> node.setEffect(null)); timeline.play(); } */ public static ScaleTransition zoomIn(Node node) { return zoomIn(node, 0); } public static ScaleTransition zoomIn(Node node, int delayMillis) { return scaleFromTo(node, 0.95, 1.0, delayMillis); } public static ScaleTransition explodeOut(Node node) { return scaleFromTo(node, 1.0, 1.05, 0); } private static ScaleTransition scaleFromTo(Node node, double from, double to, int delayMillis) { //node.setCache(true); //node.setCacheHint(CacheHint.SPEED); ScaleTransition scale = new ScaleTransition(Duration.millis(UI_ANIMATION_TIME_MSEC), node); scale.setFromX(from); scale.setFromY(from); scale.setToX(to); scale.setToY(to); scale.setDelay(Duration.millis(delayMillis)); //scale.setOnFinished(ev -> node.setCache(false)); scale.play(); return scale; } public static void dropShadowOn(Node node) { DropShadow dropShadow = node.getEffect() != null ? (DropShadow) node.getEffect() : new DropShadow(BlurType.THREE_PASS_BOX, Color.BLACK, 0.0, 0.0, 0, 0); node.setEffect(dropShadow); Timeline timeline = new Timeline(); timeline.getKeyFrames().add( new KeyFrame(Duration.millis(UI_ANIMATION_TIME_MSEC / 3), new KeyValue(dropShadow.radiusProperty(), 3.0)) ); timeline.play(); } public static void dropShadowOff(Node node) { DropShadow dropShadow = (DropShadow) node.getEffect(); Timeline timeline = new Timeline(); timeline.getKeyFrames().add( new KeyFrame(Duration.millis(UI_ANIMATION_TIME_MSEC / 3), new KeyValue(dropShadow.radiusProperty(), 0.0)) ); timeline.setOnFinished((ev) -> node.setEffect(null)); timeline.play(); } public static void brightnessAdjust(Node node, double adjustment) { node.setCache(true); node.setCacheHint(CacheHint.SPEED); ColorAdjust adjust = new ColorAdjust(); adjust.setBrightness(0.0); node.setEffect(adjust); Timeline timeline = new Timeline(new KeyFrame(Duration.millis(UI_ANIMATION_TIME_MSEC * 0.7), new KeyValue(adjust.brightnessProperty(), adjustment))); timeline.play(); } public static void brightnessUnadjust(Node node) { ColorAdjust effect = (ColorAdjust) node.getEffect(); Timeline timeline = new Timeline(new KeyFrame(Duration.millis(UI_ANIMATION_TIME_MSEC * 0.7), new KeyValue(effect.brightnessProperty(), 0.0))); timeline.setOnFinished(ev -> node.setCache(false)); timeline.play(); } public static void checkGuiThread() { if (!Platform.isFxApplicationThread()) { // Don't just throw directly here to avoid missing the problem when buggy code swallows the exceptions. IllegalStateException ex = new IllegalStateException(); log.error("Threading violation: not on FX UI thread", ex); CrashWindow.open(ex); throw ex; } } public static BooleanBinding conjunction(List<BooleanProperty> list) { BooleanBinding accumulator = new SimpleBooleanProperty(true).and(list.get(0)); for (int i = 1; i < list.size(); i++) { accumulator = accumulator.and(list.get(i)); } return accumulator; } public static Path resourceOverrideDirectory; public static URL getResource(String name) { if (resourceOverrideDirectory != null) return unchecked(() -> new URL("file://" + resourceOverrideDirectory.resolve(name).toAbsolutePath())); else return Main.class.getResource(name); } @Nullable public static Coin valueOrNull(String str) { try { return valueOrThrow(str); } catch (NumberFormatException e) { return null; } } public static Coin valueOrThrow(String str) throws NumberFormatException { long value = BitcoinValue.userInputToSatoshis(str); if (value > 0) return Coin.valueOf(value); throw new NumberFormatException(); } public static void runOnGuiThreadAfter(long millis, Runnable runnable) { new Thread(() -> { Uninterruptibles.sleepUninterruptibly(millis, TimeUnit.MILLISECONDS); Platform.runLater(runnable); }).start(); } public static void runAfterFrame(Runnable runnable) { AnimationTimer frameWaiter = new AnimationTimer() { private int frames; @Override public void handle(long l) { frames++; if (frames > 2) { stop(); runnable.run(); } } }; frameWaiter.start(); } public static void platformFiddleChooser(FileChooser chooser) { // Work around FileChooser bugs. if (LHUtils.isUnix()) { chooser.setInitialDirectory(new File(System.getProperty("user.home"))); } } public static void platformFiddleChooser(DirectoryChooser chooser) { // Work around DirectoryChooser bugs. if (LHUtils.isUnix()) { chooser.setInitialDirectory(new File(System.getProperty("user.home"))); } } public static void roundCorners(ImageView view, double amount) { // This should be easier to do just with CSS. Rectangle clipRect = new Rectangle(view.getFitWidth(), view.getFitHeight()); clipRect.setArcWidth(amount); clipRect.setArcHeight(amount); view.setClip(clipRect); } private static HashMap<Node, Runnable> currentPops = new HashMap<>(); public static CompletableFuture<Void> arrowBubbleToNode(Node target, String text) { checkGuiThread(); // Make any bubble that's currently pointing to the same node finish up. if (currentPops.get(target) != null) currentPops.get(target).run(); Label content = new Label(text); content.setStyle("-fx-font-size: 12; -fx-padding: 0 20 0 20"); PopOver popover = new PopOver(content); popover.setDetachable(false); popover.setArrowLocation(PopOver.ArrowLocation.TOP_CENTER); popover.show(target); CompletableFuture<Void> future = new CompletableFuture<>(); Runnable finish = new Runnable() { private boolean ran = false; @Override public void run() { checkGuiThread(); if (!ran) { currentPops.remove(target); ran = true; popover.hide(); runOnGuiThreadAfter(200 /* from PopOver sources */, () -> { if (!future.isDone()) future.complete(null); }); } } }; currentPops.put(target, finish); // Make bubble disappear after 4 seconds. runOnGuiThreadAfter(4000, finish); // Make bubble disappear if the node it's pointing to disappears. target.sceneProperty().addListener((observable, oldValue, newValue) -> { if (newValue == null) finish.run(); }); return future; } public static class AnimatedBindInfo { @Nullable public Timeline timeline; public NumberBinding bindFrom; public Runnable onAnimFinish; } public static AnimatedBindInfo animatedBind(Node node, WritableDoubleValue bindTo, NumberBinding bindFrom) { return animatedBind(node, bindTo, bindFrom, null); } public static AnimatedBindInfo animatedBind(Node node, WritableDoubleValue bindTo, NumberBinding bindFrom, @Nullable Interpolator interpolator) { bindTo.set(bindFrom.doubleValue()); // Initialise. bindFrom.addListener((o, prev, cur) -> { AnimatedBindInfo info = (AnimatedBindInfo) node.getUserData(); if (info.timeline != null) info.timeline.stop(); info.timeline = new Timeline(new KeyFrame(UI_ANIMATION_TIME, interpolator != null ? new KeyValue(bindTo, cur, interpolator) : new KeyValue(bindTo, cur))); info.timeline.setOnFinished(ev -> { ((AnimatedBindInfo) node.getUserData()).timeline = null; if (info.onAnimFinish != null) info.onAnimFinish.run(); }); info.timeline.play(); }); // We must pin bindFrom into the object graph, otherwise something like: // animatedBind(node, node.opacityProperty(), when(a).then(1).otherwise(2)) // will mysteriously stop working when the result of when() gets garbage collected and the listener with it. AnimatedBindInfo info = new AnimatedBindInfo(); info.bindFrom = bindFrom; node.setUserData(info); return info; } public static boolean isSoftwarePipeline() { return GraphicsPipeline.getPipeline() instanceof SWPipeline; } }