onAction) {
+ this.onAction = onAction;
+ return this;
+ }
+
+ /**
+ * Specify that the notification should use the built-in dark styling,
+ * rather than the default 'modena' notification style (which is a
+ * light-gray).
+ */
+ public
+ Growl darkStyle() {
+ styleClass.add(STYLE_CLASS_DARK);
+ return this;
+ }
+
+ /**
+ * Specify that the close button in the top-right corner of the notification
+ * should not be shown.
+ */
+ public
+ Growl hideCloseButton() {
+ this.hideCloseButton = true;
+ return this;
+ }
+
+ /**
+ * Instructs the notification to be shown, and that it should use the built-in 'warning' graphic.
+ */
+ public
+ void showWarning() {
+ graphic(new ImageView(Growl.class.getResource("/org/controlsfx/dialog/dialog-warning.png")
+ .toExternalForm())); //$NON-NLS-1$
+ show();
+ }
+
+ /**
+ * Instructs the notification to be shown, and that it should use the built-in 'information' graphic.
+ */
+ public
+ void showInformation() {
+ graphic(new ImageView(Growl.class.getResource("/org/controlsfx/dialog/dialog-information.png")
+ .toExternalForm())); //$NON-NLS-1$
+ show();
+ }
+
+ /**
+ * Instructs the notification to be shown, and that it should use the built-in 'error' graphic.
+ */
+ public
+ void showError() {
+ graphic(new ImageView(Growl.class.getResource("/org/controlsfx/dialog/dialog-error.png")
+ .toExternalForm())); //$NON-NLS-1$
+ show();
+ }
+
+ /**
+ * Instructs the notification to be shown, and that it should use the built-in 'confirm' graphic.
+ */
+ public
+ void showConfirm() {
+ graphic(new ImageView(Growl.class.getResource("/org/controlsfx/dialog/dialog-confirm.png")
+ .toExternalForm())); //$NON-NLS-1$
+ show();
+ }
+
+ /**
+ * Instructs the notification to be shown.
+ */
+ public
+ void show() {
+ // we can't use regular popup, because IF WE HAVE NO OWNER, it won't work!
+ // instead, we just create a JFRAME (and use our StageViaSwing class) to put javaFX inside it
+ JavaFxUtil.invokeAndWait(() -> new GrowlPopup(this).show());
+ }
+
+
+}
+
diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/GrowlNotification.java b/Dorkbox-Util/src/dorkbox/util/javafx/GrowlNotification.java
new file mode 100644
index 0000000..10e6efb
--- /dev/null
+++ b/Dorkbox-Util/src/dorkbox/util/javafx/GrowlNotification.java
@@ -0,0 +1,169 @@
+/**
+ * Copyright (c) 2014, ControlsFX All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are
+ * met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution. * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CONTROLSFX
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * MODIFIED BY DORKBOX, LLC Copyright 2015 dorkbox, llc
+ *
+ * Licensed 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 dorkbox.util.javafx;
+
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+
+public
+class GrowlNotification extends Region {
+
+ private static final double MIN_HEIGHT = 40;
+
+ private final String textText;
+ private final Node graphicNode;
+
+ protected final GridPane pane;
+
+ public
+ GrowlNotification(final Growl notification) {
+ this.textText = notification.text;
+ this.graphicNode = notification.graphic;
+
+ getStyleClass().add("notification-bar"); //$NON-NLS-1$
+
+ setVisible(true);
+
+ pane = new GridPane();
+ pane.getStyleClass()
+ .add("pane"); //$NON-NLS-1$
+ pane.setAlignment(Pos.BASELINE_LEFT);
+ getChildren().setAll(pane);
+
+ // put it all together
+ pane.getChildren()
+ .clear();
+
+ int row = 0;
+
+ // title
+ if (notification.title != null && !notification.title.isEmpty()) {
+ Label title = new Label();
+ title.getStyleClass()
+ .add("title"); //$NON-NLS-1$
+ title.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+ GridPane.setHgrow(title, Priority.ALWAYS);
+
+ title.setText(notification.title);
+ pane.add(title, 0, row++);
+ }
+
+ Region spacer = new Region();
+ spacer.setPrefHeight(10);
+
+ pane.add(spacer, 0, row++);
+
+ // graphic + text area
+ Label label = new Label();
+ label.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+ GridPane.setVgrow(label, Priority.ALWAYS);
+ GridPane.setHgrow(label, Priority.ALWAYS);
+
+ label.setText(textText);
+ label.setGraphic(graphicNode);
+ pane.add(label, 0, row);
+
+
+ // close button
+ if (!notification.hideCloseButton) {
+ Button closeBtn = new Button();
+ closeBtn.getStyleClass()
+ .setAll("close-button"); //$NON-NLS-1$
+
+ StackPane graphic = new StackPane();
+ graphic.getStyleClass()
+ .setAll("graphic"); //$NON-NLS-1$
+
+ closeBtn.setGraphic(graphic);
+ closeBtn.setMinSize(17, 17);
+ closeBtn.setPrefSize(17, 17);
+
+ GridPane.setMargin(closeBtn, new Insets(0, 0, 0, 8));
+
+ // position the close button in the best place, depending on the height
+ double minHeight = minHeight(-1);
+ GridPane.setValignment(closeBtn, minHeight == MIN_HEIGHT ? VPos.CENTER : VPos.TOP);
+ closeBtn.setOnAction(arg0 -> hide());
+
+ pane.add(closeBtn, 2, 0, 1, row + 1);
+ }
+ }
+
+ public
+ void hide() {
+ }
+
+ @Override
+ protected
+ void layoutChildren() {
+ final double w = getWidth();
+ double h = computePrefHeight(-1);
+
+ pane.resize(w, h);
+ }
+
+ @Override
+ protected
+ double computeMinWidth(double height) {
+ String text = textText;
+ Node graphic = graphicNode;
+
+ if ((text == null || text.isEmpty()) && (graphic != null)) {
+ return graphic.minWidth(height) + 20;
+ }
+ return 400;
+ }
+
+ @Override
+ protected
+ double computeMinHeight(double width) {
+ String text = textText;
+ Node graphic = graphicNode;
+
+ if ((text == null || text.isEmpty()) && (graphic != null)) {
+ return graphic.minHeight(width) + 20;
+ }
+ return 100;
+ }
+
+ @Override
+ protected
+ double computePrefHeight(double width) {
+ return Math.max(pane.prefHeight(width), minHeight(width));
+ }
+}
+
diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/GrowlPopup.java b/Dorkbox-Util/src/dorkbox/util/javafx/GrowlPopup.java
new file mode 100644
index 0000000..6b6829e
--- /dev/null
+++ b/Dorkbox-Util/src/dorkbox/util/javafx/GrowlPopup.java
@@ -0,0 +1,467 @@
+package dorkbox.util.javafx;
+
+import dorkbox.util.ScreenUtil;
+import dorkbox.util.SwingUtil;
+import javafx.animation.*;
+import javafx.collections.ObservableList;
+import javafx.embed.swing.SwingFXUtils;
+import javafx.geometry.Pos;
+import javafx.geometry.VPos;
+import javafx.scene.Scene;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+import org.controlsfx.tools.Utils;
+
+import java.awt.*;
+import java.awt.geom.*;
+import java.util.ArrayList;
+
+/**
+ *
+ */
+public
+class GrowlPopup {
+
+ private static final java.util.List popups = new ArrayList<>();
+ // for animating in the notifications
+ private static final ParallelTransition parallelTransition = new ParallelTransition();
+
+ private static final double padding = 40;
+
+ private final GrowlPopupViaSwing frame;
+
+ final double startX;
+ final double startY;
+ final javafx.stage.Window window;
+ final double screenWidth;
+ final double screenHeight;
+
+ private final Pos position;
+
+ private final double anchorX;
+ private final double anchorY;
+
+ final Timeline animationTimeline = new Timeline();
+ private double newX;
+ private double newY;
+
+ GrowlPopup(final Growl notification) {
+ final Image icon;
+ if (notification.graphic instanceof ImageView) {
+ icon = SwingFXUtils.fromFXImage(((ImageView) notification.graphic).getImage(), null);
+ } else {
+ icon = SwingUtil.BLANK_ICON;
+ }
+
+ // created on the swing EDT
+ frame = GrowlPopupViaSwing.create(icon, notification.title);
+ // don't actually show anything. This will be done by our own animator
+ frame.setShowAnimation(() -> {
+ frame.completeShowTransition();
+ });
+
+ // set screen position
+ final javafx.stage.Window owner = notification.owner;
+ if (owner == null) {
+ final Point mouseLocation = MouseInfo.getPointerInfo()
+ .getLocation();
+
+ final GraphicsDevice deviceAtMouse = ScreenUtil.getGraphicsDeviceAt(mouseLocation);
+
+ final Rectangle screenBounds = deviceAtMouse.getDefaultConfiguration()
+ .getBounds();
+
+ /*
+ * If the owner is not set, we work with the whole screen.
+ * EDIT: we use the screen that the mouse is currently on.
+ */
+ startX = screenBounds.getX();
+ startY = screenBounds.getY();
+ screenWidth = screenBounds.getWidth();
+ screenHeight = screenBounds.getHeight();
+
+ window = Utils.getWindow(null);
+ }
+ else {
+ /*
+ * If the owner is set, we will make the notifications popup
+ * inside its window.
+ */
+ startX = owner.getX();
+ startY = owner.getY();
+ screenWidth = owner.getWidth();
+ screenHeight = owner.getHeight();
+ window = owner;
+ }
+
+
+ // need to install our CSS
+ if (owner instanceof Stage) {
+ Scene ownerScene = owner.getScene();
+ ownerScene.getStylesheets()
+ .add(org.controlsfx.control.Notifications.class.getResource("notificationpopup.css")
+ .toExternalForm()); //$NON-NLS-1$
+ }
+
+
+ this.position = notification.position;
+
+
+ VBox region = new VBox();
+ final ObservableList styleClass1 = region.getStyleClass();
+ styleClass1.add("notification-bar");
+ styleClass1.addAll(notification.styleClass);
+
+ region.setVisible(true);
+ region.setMinWidth(300);
+ region.setMinHeight(40);
+
+
+ GridPane pane = new GridPane();
+ pane.getStyleClass()
+ .add("pane");
+ pane.setAlignment(Pos.BASELINE_LEFT);
+ region.getChildren()
+ .add(pane);
+
+// pane.setStyle("-fx-background-color: #2046ff;");
+
+ // title
+ if (notification.title != null && !notification.title.isEmpty()) {
+ javafx.scene.control.Label titleLabel = new javafx.scene.control.Label();
+ titleLabel.getStyleClass()
+ .add("title");
+ titleLabel.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+ GridPane.setHgrow(titleLabel, Priority.ALWAYS);
+
+ titleLabel.setText(notification.title);
+ pane.add(titleLabel, 0, 0);
+ }
+
+
+ // close button
+ if (!notification.hideCloseButton) {
+ javafx.scene.control.Button closeBtn = new javafx.scene.control.Button();
+ closeBtn.getStyleClass()
+ .setAll("close-button");
+
+ StackPane graphic = new StackPane();
+ graphic.getStyleClass()
+ .setAll("graphic");
+
+ closeBtn.setGraphic(graphic);
+ closeBtn.setMinSize(17, 17);
+ closeBtn.setPrefSize(17, 17);
+
+ GridPane.setMargin(closeBtn, new javafx.geometry.Insets(0, 0, 0, 8));
+
+ // position the close button in the best place, depending on the height
+ double minHeight = pane.minHeight(-1);
+ GridPane.setValignment(closeBtn, minHeight == 40 ? VPos.CENTER : VPos.TOP);
+
+ closeBtn.setOnAction(arg0 -> createHideTimeline(Duration.ZERO).play());
+
+ pane.add(closeBtn, 2, 0, 1, 1);
+ }
+
+
+ // graphic + text area
+ javafx.scene.control.Label label = new javafx.scene.control.Label();
+ label.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+ GridPane.setVgrow(label, Priority.ALWAYS);
+ GridPane.setHgrow(label, Priority.ALWAYS);
+
+ label.setText(notification.text);
+ label.setGraphic(notification.graphic);
+ label.setPadding(new javafx.geometry.Insets(10, 0, 10, 5));
+ pane.add(label, 0, 2);
+
+
+ region.setOnMouseClicked(e -> createHideTimeline(Duration.ZERO).play());
+
+ Scene scene = new Scene(region);
+ scene.getStylesheets()
+ .add(org.controlsfx.control.Notifications.class.getResource("notificationpopup.css")
+ .toExternalForm()); //$NON-NLS-1$
+ frame.setScene(scene);
+
+ frame.sizeToScene();
+
+ // determine location for the popup
+ final Dimension size = frame.getSize();
+ final double barWidth = size.getWidth();
+ final double barHeight = size.getHeight();
+
+ // get anchorX
+ switch (position) {
+ case TOP_LEFT:
+ case CENTER_LEFT:
+ case BOTTOM_LEFT:
+ anchorX = startX + padding;
+ break;
+
+ case TOP_CENTER:
+ case CENTER:
+ case BOTTOM_CENTER:
+ anchorX = startX + (screenWidth / 2.0) - barWidth / 2.0 - padding / 2.0;
+ break;
+
+ default:
+ case TOP_RIGHT:
+ case CENTER_RIGHT:
+ case BOTTOM_RIGHT:
+ anchorX = startX + screenWidth - barWidth - padding;
+ break;
+ }
+
+ // get anchorY
+ switch (position) {
+ case TOP_LEFT:
+ case TOP_CENTER:
+ case TOP_RIGHT:
+ anchorY = padding + startY;
+ break;
+
+ case CENTER_LEFT:
+ case CENTER:
+ case CENTER_RIGHT:
+ anchorY = startY + (screenHeight / 2.0) - barHeight / 2.0 - padding / 2.0;
+ break;
+
+ default:
+ case BOTTOM_LEFT:
+ case BOTTOM_CENTER:
+ case BOTTOM_RIGHT:
+ anchorY = startY + screenHeight - barHeight - padding;
+ break;
+ }
+ }
+
+ public
+ void show() {
+ this.newX = anchorX;
+ this.newY = anchorY;
+ frame.show(anchorX, anchorY);
+
+ addPopupToMap();
+
+ // begin a timeline to get rid of the popup (default is 5 seconds)
+// if (notification.hideAfterDuration != Duration.INDEFINITE) {
+// Timeline timeline = createHideTimeline(popup, growlNotification, p, notification.hideAfterDuration);
+// timeline.play();
+// }
+ }
+
+ @Override
+ public
+ boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final GrowlPopup that = (GrowlPopup) o;
+ return frame.equals(that.frame);
+ }
+
+ @Override
+ public
+ int hashCode() {
+ return frame.hashCode();
+ }
+
+ void close() {
+ // set it off screen (which is what the close method also does)
+ this.newX = Short.MIN_VALUE;
+ this.newY = Short.MIN_VALUE;
+
+ frame.close();
+ }
+
+ Dimension2D getSize() {
+ return frame.getSize();
+ }
+
+ void animateToTarget(final boolean shouldFadeIn, final double x, final double y) {
+
+ if (shouldFadeIn) {
+ if (frame.getOpacityProperty().getValue() == 0F) {
+ frame.setLocation((int)x, (int)y);
+ Timeline timeline = new Timeline();
+ timeline.getKeyFrames()
+ .addAll(new KeyFrame(Duration.millis(500), new KeyValue(frame.getOpacityProperty(), 1F, Interpolator.LINEAR)));
+ timeline.play();
+ }
+ } else {
+ frame.setLocation((int)x, (int)y);
+
+// final boolean xEqual = x == frame.getX();
+// final boolean yEqual = y == frame.getY();
+//
+// if (xEqual && yEqual) {
+// return;
+// }
+//Transition t = new Transition() {
+// {
+// setCycleDuration(Duration.millis(500));
+// }
+//
+// @Override
+// protected
+// void interpolate(final double frac) {
+// final double y1 = frame.getY();
+// final double distance = ((y-y1) * frac);
+//
+// frame.setLocation(x, y1 + distance);
+// }
+// };
+// parallelTransition.getChildren().add(t);
+ }
+
+// final ObservableList keyFrames = animationTimeline.getKeyFrames();
+// keyFrames.clear();
+//
+// if (!xEqual) {
+// keyFrames.addAll(new KeyFrame(Duration.millis(300), new KeyValue(xProperty, x, Interpolator.EASE_OUT)));
+// }
+// if (!yEqual) {
+// keyFrames.addAll(new KeyFrame(Duration.millis(300), new KeyValue(yProperty, y, Interpolator.EASE_OUT)));
+// }
+//
+// // x/y can change, keep running the animation until it's stable
+// animationTimeline.setOnFinished(event -> animateToTarget(GrowlPopup.this.newX, GrowlPopup.this.newY));
+// animationTimeline.playFromStart();
+// }
+// }
+ }
+
+ private
+ Timeline createHideTimeline(final Duration startDelay) {
+ Timeline timeline = new Timeline(new KeyFrame(Duration.millis(500), new KeyValue(frame.getOpacityProperty(), 0.0F)));
+ timeline.setDelay(startDelay);
+ timeline.setOnFinished(e -> {
+ close();
+ removePopupFromMap();
+ });
+
+ return timeline;
+ }
+
+ // only called on the JavaFX app thread
+ private
+ void addPopupToMap() {
+ popups.add(this);
+ doAnimation(true);
+ }
+
+ // only called on the JavaFX app thread
+ private
+ void removePopupFromMap() {
+ popups.remove(this);
+
+ if (!popups.isEmpty()) {
+ doAnimation(false);
+ }
+ }
+
+ // only called on the JavaFX app thread
+ private static
+ void doAnimation(boolean shouldFadeIn) {
+ parallelTransition.stop();
+ parallelTransition.getChildren()
+ .clear();
+
+
+ // the logic for this, is that the first popup in place, doesn't move. EVERY other popup after it will be moved
+ // this behavior trickles down to the remaining popups, until all popups have been assigned new locations
+
+ final int length = popups.size();
+ final GrowlPopup[] copies = popups.toArray(new GrowlPopup[length]);
+
+ for (int i = 0; i < length; i++) {
+ final GrowlPopup popup = copies[i];
+ final boolean isShowFromTop = isShowFromTop(popup.position);
+
+ final Dimension2D size = popup.getSize();
+ final double x = popup.newX;
+ final double y = popup.newY;
+ final double width = size.getWidth();
+ final double height = size.getHeight();
+
+ if (isShowFromTop) {
+ for (int j = i+1; j < length; j++) {
+ final GrowlPopup copy = copies[j];
+
+ final Dimension2D size1 = copy.getSize();
+ final double x1 = copy.newX;
+ final double y1 = copy.newY;
+ final double width1 = size1.getWidth();
+ final double height1 = size1.getHeight();
+
+ if (intersectRect(x, y, width, height, x1, y1, width1, height1)) {
+ copy.newY = y + height + 10;
+ }
+ }
+
+ popup.animateToTarget(shouldFadeIn, popup.newX, popup.newY);
+ }
+
+//
+// // first one is always as base location with padding
+// if (i == 0) {
+// newY = 30 + _popup.startY;
+// }
+// else {
+// // we add a little bit of padding, so they are not on top of eachother
+// newY += popupHeight + 10;
+// }
+// }
+// else {
+// if (i == size - 1) {
+//// newY = changedPopup.getTargetY() - popupHeight;
+// }
+// else {
+// newY -= popupHeight;
+// }
+// }
+//
+// if (newY < 0) {
+// System.err.println("closing");
+// _popup.close();
+// continue;
+// }
+
+// popup.animateToTarget(popup.anchorX, newY);
+ }
+
+ if (!parallelTransition.getChildren().isEmpty()) {
+// parallelTransition.play();
+ }
+ }
+
+ static boolean intersectRect(double x1, double y1, double w1, double h1, double x2, double y2, double w2, double h2) {
+ return intersectRange(x1, x1+w1, x2, x2+w2) && intersectRange(y1, y1+h1, y2, y2+h2);
+ }
+ static boolean intersectRange(double ax1, double ax2, double bx1, double bx2) {
+ return Math.max(ax1, bx1) <= Math.min(ax2, bx2);
+ }
+
+ private static
+ boolean isShowFromTop(final Pos p) {
+ switch (p) {
+ case TOP_LEFT:
+ case TOP_CENTER:
+ case TOP_RIGHT:
+ return true;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/GrowlPopupViaSwing.java b/Dorkbox-Util/src/dorkbox/util/javafx/GrowlPopupViaSwing.java
new file mode 100644
index 0000000..cd4c4ab
--- /dev/null
+++ b/Dorkbox-Util/src/dorkbox/util/javafx/GrowlPopupViaSwing.java
@@ -0,0 +1,70 @@
+package dorkbox.util.javafx;
+
+import dorkbox.util.SwingUtil;
+
+import java.awt.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ *
+ */
+public
+class GrowlPopupViaSwing extends StageViaSwing {
+
+ private static
+ AtomicInteger ID = new AtomicInteger(0);
+
+ @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
+ static
+ GrowlPopupViaSwing create(final Image icon, final String title) {
+ final GrowlPopupViaSwing[] returnVal = new GrowlPopupViaSwing[1];
+
+ // this MUST happen on the EDT!
+ SwingUtil.invokeAndWait(() -> {
+ synchronized (returnVal) {
+ returnVal[0] = new GrowlPopupViaSwing(icon, title, ID.getAndIncrement());
+ }
+ });
+
+ synchronized (returnVal) {
+ return returnVal[0];
+ }
+ }
+
+ private final int id;
+
+
+
+ GrowlPopupViaSwing(final Image icon, final String title, final int ID) {
+ super();
+
+ this.id = ID;
+
+ frame.setAlwaysOnTop(true);
+ frame.setResizable(false);
+ frame.setIconImage(icon);
+ frame.setTitle(title);
+ }
+
+ @Override
+ public
+ boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final GrowlPopupViaSwing that = (GrowlPopupViaSwing) o;
+
+ return id == that.id;
+
+ }
+
+ @Override
+ public
+ int hashCode() {
+ return id;
+ }
+}
diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/Notifications.java b/Dorkbox-Util/src/dorkbox/util/javafx/Notifications.java
deleted file mode 100644
index af1282a..0000000
--- a/Dorkbox-Util/src/dorkbox/util/javafx/Notifications.java
+++ /dev/null
@@ -1,631 +0,0 @@
-/**
- * Copyright (c) 2014, ControlsFX
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- * * Neither the name of ControlsFX, any associated website, nor the
- * names of its contributors may be used to endorse or promote products
- * derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- *
- * MODIFIED BY DORKBOX, LLC
- */
-package dorkbox.util.javafx;
-
-import dorkbox.util.ScreenUtil;
-import impl.org.controlsfx.skin.NotificationBar;
-import javafx.animation.*;
-import javafx.collections.FXCollections;
-import javafx.collections.ObservableList;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
-import javafx.geometry.Pos;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.image.ImageView;
-import javafx.stage.Popup;
-import javafx.stage.Stage;
-import javafx.stage.Window;
-import javafx.util.Duration;
-import org.controlsfx.control.action.Action;
-import org.controlsfx.tools.Utils;
-
-import java.awt.*;
-import java.util.*;
-import java.util.List;
-
-/**
- * An API to show popup notification messages to the user in the corner of their
- * screen, unlike the {@link org.controlsfx.control.NotificationPane} which shows notification messages
- * within your application itself.
- *
- * Screenshot
- *
- * The following screenshot shows a sample notification rising from the
- * bottom-right corner of my screen:
- *
- *
- *
- *
- *
- *
Code Example:
- *
- * To create the notification shown in the screenshot, simply do the following:
- *
- *
- * {@code
- * Notifications.create()
- * .title("Title Text")
- * .text("Hello World 0!")
- * .showWarning();
- * }
- *
- */
-public class Notifications {
-
- /***************************************************************************
- * * Static fields * *
- **************************************************************************/
-
- private static final String STYLE_CLASS_DARK = "dark"; //$NON-NLS-1$
-
- /***************************************************************************
- * * Private fields * *
- **************************************************************************/
-
- private String title;
- private String text;
- private Node graphic;
- private ObservableList actions = FXCollections.observableArrayList();
- private Pos position = Pos.BOTTOM_RIGHT;
- private Duration hideAfterDuration = Duration.seconds(5);
- private boolean hideCloseButton;
- private EventHandler onAction;
- private Window owner;
-
- private List styleClass = new ArrayList<>();
-
- /***************************************************************************
- * * Constructors * *
- **************************************************************************/
-
- // we do not allow instantiation of the Notifications class directly - users
- // must go via the builder API (that is, calling create())
- private Notifications() {
- // no-op
- }
-
- /***************************************************************************
- * * Public API * *
- **************************************************************************/
-
- /**
- * Call this to begin the process of building a notification to show.
- */
- public static
- Notifications create() {
- return new Notifications();
- }
-
- /**
- * Specify the text to show in the notification.
- */
- public
- Notifications text(String text) {
- this.text = text;
- return this;
- }
-
- /**
- * Specify the title to show in the notification.
- */
- public
- Notifications title(String title) {
- this.title = title;
- return this;
- }
-
- /**
- * Specify the graphic to show in the notification.
- */
- public
- Notifications graphic(Node graphic) {
- this.graphic = graphic;
- return this;
- }
-
- /**
- * Specify the position of the notification on screen, by default it is
- * {@link Pos#BOTTOM_RIGHT bottom-right}.
- */
- public
- Notifications position(Pos position) {
- this.position = position;
- return this;
- }
-
- /**
- * The dialog window owner - if specified the notifications will be inside
- * the owner, otherwise the notifications will be shown within the whole
- * screen.
- */
- public
- Notifications owner(Object owner) {
- this.owner = Utils.getWindow(owner);
- return this;
- }
-
- /**
- * Specify the duration that the notification should show, after which it
- * will be hidden.
- */
- public
- Notifications hideAfter(Duration duration) {
- this.hideAfterDuration = duration;
- return this;
- }
-
- /**
- * Specify what to do when the user clicks on the notification (in addition
- * to the notification hiding, which happens whenever the notification is
- * clicked on).
- */
- public
- Notifications onAction(EventHandler onAction) {
- this.onAction = onAction;
- return this;
- }
-
- /**
- * Specify that the notification should use the built-in dark styling,
- * rather than the default 'modena' notification style (which is a
- * light-gray).
- */
- public
- Notifications darkStyle() {
- styleClass.add(STYLE_CLASS_DARK);
- return this;
- }
-
- /**
- * Specify that the close button in the top-right corner of the notification
- * should not be shown.
- */
- public
- Notifications hideCloseButton() {
- this.hideCloseButton = true;
- return this;
- }
-
- /**
- * Specify the actions that should be shown in the notification as buttons.
- */
- public
- Notifications action(Action... actions) {
- this.actions = actions == null ? FXCollections. observableArrayList() : FXCollections
- .observableArrayList(actions);
- return this;
- }
-
- /**
- * Instructs the notification to be shown, and that it should use the
- * built-in 'warning' graphic.
- */
- public void showWarning() {
- graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-warning.png").toExternalForm())); //$NON-NLS-1$
- show();
- }
-
- /**
- * Instructs the notification to be shown, and that it should use the
- * built-in 'information' graphic.
- */
- public void showInformation() {
- graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-information.png").toExternalForm())); //$NON-NLS-1$
- show();
- }
-
- /**
- * Instructs the notification to be shown, and that it should use the
- * built-in 'error' graphic.
- */
- public void showError() {
- graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-error.png").toExternalForm())); //$NON-NLS-1$
- show();
- }
-
- /**
- * Instructs the notification to be shown, and that it should use the
- * built-in 'confirm' graphic.
- */
- public void showConfirm() {
- graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-confirm.png").toExternalForm())); //$NON-NLS-1$
- show();
- }
-
- /**
- * Instructs the notification to be shown.
- */
- public void show() {
- NotificationPopupHandler.getInstance().show(this);
- }
-
- /***************************************************************************
- * * Private support classes * *
- **************************************************************************/
-
- // not public so no need for JavaDoc
- private static final
- class NotificationPopupHandler {
-
- private static final NotificationPopupHandler INSTANCE = new NotificationPopupHandler();
-
- static final
- NotificationPopupHandler getInstance() {
- return INSTANCE;
- }
- private final Map> popupsMap = new HashMap<>();
- private final double padding = 15;
-
- // for animating in the notifications
- private final ParallelTransition parallelTransition = new ParallelTransition();
-
- private double startX;
- private double startY;
- private double screenWidth;
- private double screenHeight;
-
- private boolean isShowing = false;
-
- public
- void show(Notifications notification) {
- Window window;
- final Window owner = notification.owner;
- if (owner == null) {
- Point mouseLocation = MouseInfo.getPointerInfo()
- .getLocation();
-
- GraphicsDevice deviceAtMouse = ScreenUtil.getGraphicsDeviceAt(mouseLocation);
-
- final Rectangle screenBounds = deviceAtMouse.getDefaultConfiguration()
- .getBounds();
-
- /*
- * If the owner is not set, we work with the whole screen.
- * EDIT: we use the screen that the mouse is currently on.
- */
- startX = screenBounds.getX();
- startY = screenBounds.getY();
- screenWidth = screenBounds.getWidth();
- screenHeight = screenBounds.getHeight();
-
- window = Utils.getWindow(null);
- }
- else {
- /*
- * If the owner is set, we will make the notifications popup
- * inside its window.
- */
- startX = owner.getX();
- startY = owner.getY();
- screenWidth = owner.getWidth();
- screenHeight = owner.getHeight();
- window = owner;
- }
- show(window, notification);
- }
-
- private void show(Window owner, final Notifications notification) {
- // need to install our CSS
- if (owner instanceof Stage) {
- Scene ownerScene = ((Stage) owner).getScene();
- ownerScene.getStylesheets().add(org.controlsfx.control.Notifications.class.getResource("notificationpopup.css")
- .toExternalForm()); //$NON-NLS-1$
- }
-
- final Popup popup = new Popup();
- popup.setAutoFix(false);
-
- final Pos p = notification.position;
-
- final NotificationBar notificationBar = new NotificationBar() {
- @Override public String getTitle() {
- return notification.title;
- }
-
- @Override public String getText() {
- return notification.text;
- }
-
- @Override public Node getGraphic() {
- return notification.graphic;
- }
-
- @Override public ObservableList getActions() {
- return notification.actions;
- }
-
- @Override public boolean isShowing() {
- return isShowing;
- }
-
- @Override protected double computeMinWidth(double height) {
- String text = getText();
- Node graphic = getGraphic();
- if ((text == null || text.isEmpty()) && (graphic != null)) {
- return graphic.minWidth(height);
- }
- return 400;
- }
-
- @Override protected double computeMinHeight(double width) {
- String text = getText();
- Node graphic = getGraphic();
- if ((text == null || text.isEmpty()) && (graphic != null)) {
- return graphic.minHeight(width);
- }
- return 100;
- }
-
- @Override public boolean isShowFromTop() {
- return NotificationPopupHandler.this.isShowFromTop(notification.position);
- }
-
- @Override public void hide() {
- isShowing = false;
-
- // this would slide the notification bar out of view,
- // but I prefer the fade out below
- // doHide();
-
- // animate out the popup by fading it
- createHideTimeline(popup, this, p, Duration.ZERO).play();
- }
-
- @Override public boolean isCloseButtonVisible() {
- return !notification.hideCloseButton;
- }
-
- @Override public double getContainerHeight() {
- return startY + screenHeight;
- }
-
- @Override public void relocateInParent(double x, double y) {
- // this allows for us to slide the notification upwards
- switch (p) {
- case BOTTOM_LEFT:
- case BOTTOM_CENTER:
- case BOTTOM_RIGHT:
- popup.setAnchorY(y - padding);
- break;
- default:
- // no-op
- break;
- }
- }
- };
-
- notificationBar.getStyleClass().addAll(notification.styleClass);
-
- notificationBar.setOnMouseClicked(e -> {
- if (notification.onAction != null) {
- ActionEvent actionEvent = new ActionEvent(notificationBar, notificationBar);
- notification.onAction.handle(actionEvent);
-
- // animate out the popup
- createHideTimeline(popup, notificationBar, p, Duration.ZERO).play();
- }
- });
-
- popup.getContent().add(notificationBar);
- popup.show(owner, 0, 0);
-
- // determine location for the popup
- double anchorX = 0, anchorY = 0;
- final double barWidth = notificationBar.getWidth();
- final double barHeight = notificationBar.getHeight();
-
- // get anchorX
- switch (p) {
- case TOP_LEFT:
- case CENTER_LEFT:
- case BOTTOM_LEFT:
- anchorX = padding + startX;
- break;
-
- case TOP_CENTER:
- case CENTER:
- case BOTTOM_CENTER:
- anchorX = startX + (screenWidth / 2.0) - barWidth / 2.0 - padding / 2.0;
- break;
-
- default:
- case TOP_RIGHT:
- case CENTER_RIGHT:
- case BOTTOM_RIGHT:
- anchorX = startX + screenWidth - barWidth - padding;
- break;
- }
-
- // get anchorY
- switch (p) {
- case TOP_LEFT:
- case TOP_CENTER:
- case TOP_RIGHT:
- anchorY = padding + startY;
- break;
-
- case CENTER_LEFT:
- case CENTER:
- case CENTER_RIGHT:
- anchorY = startY + (screenHeight / 2.0) - barHeight / 2.0 - padding / 2.0;
- break;
-
- default:
- case BOTTOM_LEFT:
- case BOTTOM_CENTER:
- case BOTTOM_RIGHT:
- anchorY = startY + screenHeight - barHeight - padding;
- break;
- }
-
- popup.setAnchorX(anchorX);
- popup.setAnchorY(anchorY);
-
- isShowing = true;
-
- notificationBar.doShow();
-
- addPopupToMap(p, popup);
-
- // begin a timeline to get rid of the popup
- Timeline timeline = createHideTimeline(popup, notificationBar, p, notification.hideAfterDuration);
- timeline.play();
- }
-
- private void hide(Popup popup, Pos p) {
- popup.hide();
- removePopupFromMap(p, popup);
- }
-
- private Timeline createHideTimeline(final Popup popup, NotificationBar bar, final Pos p, Duration startDelay) {
- KeyValue fadeOutBegin = new KeyValue(bar.opacityProperty(), 1.0);
- KeyValue fadeOutEnd = new KeyValue(bar.opacityProperty(), 0.0);
-
- KeyFrame kfBegin = new KeyFrame(Duration.ZERO, fadeOutBegin);
- KeyFrame kfEnd = new KeyFrame(Duration.millis(500), fadeOutEnd);
-
- Timeline timeline = new Timeline(kfBegin, kfEnd);
- timeline.setDelay(startDelay);
- timeline.setOnFinished(new EventHandler() {
- @Override
- public void handle(ActionEvent e) {
- hide(popup, p);
- }
- });
-
- return timeline;
- }
-
- private void addPopupToMap(Pos p, Popup popup) {
- List popups;
- if (!popupsMap.containsKey(p)) {
- popups = new LinkedList<>();
- popupsMap.put(p, popups);
- } else {
- popups = popupsMap.get(p);
- }
-
- doAnimation(p, popup);
-
- // add the popup to the list so it is kept in memory and can be
- // accessed later on
- popups.add(popup);
- }
-
- private void removePopupFromMap(Pos p, Popup popup) {
- if (popupsMap.containsKey(p)) {
- List popups = popupsMap.get(p);
- popups.remove(popup);
- }
- }
-
- private void doAnimation(Pos p, Popup changedPopup) {
- List popups = popupsMap.get(p);
- if (popups == null) {
- return;
- }
-
- final double newPopupHeight = changedPopup.getContent().get(0).getBoundsInParent().getHeight();
-
- parallelTransition.stop();
- parallelTransition.getChildren().clear();
-
- final boolean isShowFromTop = isShowFromTop(p);
-
- // animate all other popups in the list upwards so that the new one
- // is in the 'new' area.
- // firstly, we need to determine the target positions for all popups
- double sum = 0;
- double targetAnchors[] = new double[popups.size()];
- for (int i = popups.size() - 1; i >= 0; i--) {
- Popup _popup = popups.get(i);
-
- final double popupHeight = _popup.getContent().get(0).getBoundsInParent().getHeight();
-
- if (isShowFromTop) {
- if (i == popups.size() - 1) {
- sum = startY + newPopupHeight + padding;
- } else {
- sum += popupHeight;
- }
- targetAnchors[i] = sum;
- } else {
- if (i == popups.size() - 1) {
- sum = changedPopup.getAnchorY() - popupHeight;
- } else {
- sum -= popupHeight;
- }
-
- targetAnchors[i] = sum;
- }
- }
-
- // then we set up animations for each popup to animate towards the
- // target
- for (int i = popups.size() - 1; i >= 0; i--) {
- final Popup _popup = popups.get(i);
- final double anchorYTarget = targetAnchors[i];
- if(anchorYTarget < 0){
- _popup.hide();
- }
- final double oldAnchorY = _popup.getAnchorY();
- final double distance = anchorYTarget - oldAnchorY;
-
- Transition t = new Transition() {
- {
- setCycleDuration(Duration.millis(350));
- }
-
- @Override
- protected void interpolate(double frac) {
- double newAnchorY = oldAnchorY + distance * frac;
- _popup.setAnchorY(newAnchorY);
- }
- };
- t.setCycleCount(1);
- parallelTransition.getChildren().add(t);
- }
- parallelTransition.play();
- }
-
- private boolean isShowFromTop(Pos p) {
- switch (p) {
- case TOP_LEFT:
- case TOP_CENTER:
- case TOP_RIGHT:
- return true;
- default:
- return false;
- }
- }
- }
-}
-
diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/StageViaSwing.java b/Dorkbox-Util/src/dorkbox/util/javafx/StageViaSwing.java
index 1a3eda7..db3e6b3 100644
--- a/Dorkbox-Util/src/dorkbox/util/javafx/StageViaSwing.java
+++ b/Dorkbox-Util/src/dorkbox/util/javafx/StageViaSwing.java
@@ -17,37 +17,41 @@ package dorkbox.util.javafx;
import com.sun.javafx.application.PlatformImpl;
import dorkbox.util.JavaFxUtil;
+import dorkbox.util.NamedThreadFactory;
+import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil;
-import javafx.animation.Interpolator;
-import javafx.animation.KeyFrame;
-import javafx.animation.KeyValue;
-import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.value.WritableValue;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
-import javafx.util.Duration;
import javax.swing.*;
import java.awt.*;
-import java.awt.event.WindowAdapter;
-import java.awt.event.WindowEvent;
+import java.awt.event.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
/**
* This class is necessary, because JavaFX stage is crap on linux. This offers sort-of the same functionality, but via swing instead.
* Annoying caveat. All swing setters MUST happen on the EDT.
*/
+@SuppressWarnings("unused")
public
class StageViaSwing {
+ public static final Executor frameDisposer = Executors.newSingleThreadExecutor(new NamedThreadFactory("Swing Disposer",
+ Thread.MIN_PRIORITY,
+ true));
+
+
final JFrame frame;
final JFXPanel panel;
- private boolean inNestedEventLoop = false;
- private final CountDownLatch showlatch = new CountDownLatch(1);
- private final CountDownLatch showAndWaitlatch = new CountDownLatch(1);
+ private volatile boolean inNestedEventLoop = false;
+ private final CountDownLatch showLatch = new CountDownLatch(1);
+ private final CountDownLatch showAndWaitLatch = new CountDownLatch(1);
final WritableValue opacityProperty;
@@ -88,22 +92,41 @@ class StageViaSwing {
});
}
- private boolean center = false;
+ private volatile boolean center = false;
+ private volatile double x;
+ private volatile double y;
+ private volatile double width;
+ private volatile double height;
+ private volatile boolean closing;
+ private volatile boolean resizable;
+
+ public
+ void setAlwaysOnTop(final boolean alwaysOnTop) {
+ frame.setAlwaysOnTop(alwaysOnTop);
+ }
+
+ interface OnShowAnimation {
+ void doShow();
+}
+
+ private OnShowAnimation showAnimation = null;
- private
+ public
+ void setShowAnimation(final OnShowAnimation showAnimation) {
+ this.showAnimation = showAnimation;
+ }
+
StageViaSwing() {
- frame = new JFrame() {
-
- };
+ frame = new JFrame();
panel = new JFXPanel();
- frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+// frame.setLayout(null);
frame.setUndecorated(true);
+ frame.setOpacity(0F);
frame.add(panel);
-
opacityProperty = new WritableValue() {
@Override
public Float getValue() {
@@ -112,50 +135,122 @@ class StageViaSwing {
@Override
public void setValue(Float value) {
- SwingUtil.invokeLater(() -> frame.setOpacity(value));
+ SwingUtil.invokeAndWait(() -> frame.setOpacity(value));
}
};
frame.addWindowListener(new WindowAdapter() {
public void windowOpened(WindowEvent e) {
- Thread thread = new Thread(() -> {
- try {
- // If this runs now, it will bug out, and flash on the screen before we want it to.
- // REALLY dumb, but we have to wait for the system to draw the window and finish BEFORE we move it
- // otherwise, it'll 'flash' onscreen because it will still be in the middle of it's initial "on-show" animation.
- Thread.sleep(500);
+ if (showAnimation != null) {
+// Thread thread = new Thread(() -> {
+// try {
+ // If this runs now, it will bug out, and flash on the screen before we want it to.
+ // REALLY dumb, but we have to wait for the system to draw the window and finish BEFORE we move it
+ // otherwise, it'll 'flash' onscreen because it will still be in the middle of it's initial "on-show" animation.
+// Thread.sleep(5000);
- sizeToScene();
+ if (!inNestedEventLoop) {
+ renderContents();
+ } else {
+ // notify we are done showing, to prevent race conditions with the JFX app thread
+ // the show method continues
+ showLatch.countDown();
+ }
+// } catch(InterruptedException ignored) {
+// }
+// });
+// thread.setDaemon(true);
+// thread.setName("Window centering");
+// thread.start();
+ } else if (!inNestedEventLoop) {
+ renderContents();
+ } else {
+ // notify we are done showing, to prevent race conditions with the JFX app thread
+ // the show method continues
+ showLatch.countDown();
+ }
+ }
- if (center) {
- SwingUtil.invokeAndWait(() -> SwingUtil.showOnSameScreenAsMouseCenter(frame));
- }
+ private
+ void renderContents() {
+ sizeToScene();
- Timeline timeline = new Timeline();
- timeline.setCycleCount(1);
- timeline.getKeyFrames()
- .addAll(new KeyFrame(Duration.millis(700),
- new KeyValue(opacityProperty, 1F, Interpolator.EASE_OUT)));
- timeline.setOnFinished(event -> {
- if (inNestedEventLoop) {
- inNestedEventLoop= false;
- com.sun.javafx.tk.Toolkit.getToolkit().exitNestedEventLoop(StageViaSwing.this, null);
- } else {
- showlatch.countDown();
- }
- });
- timeline.play();
- } catch(InterruptedException ignored) {
- }
- });
- thread.setDaemon(true);
- thread.setName("Window centering");
- thread.start();
+ SwingUtil.invokeLater(StageViaSwing.this::recheckSize);
+
+ if (showAnimation == null) {
+ opacityProperty.setValue(1F);
+ completeShowTransition();
+ } else {
+ showAnimation.doShow();
+ }
}
});
}
+
+ // absolutely stupid - swing doesn't want to be forced to a certain size, unless specific incantations are performed. These seem to work
+ void recheckSize() {
+ if (frame.getX() != x || frame.getY() != y || frame.getWidth() != width || frame.getHeight() != height) {
+// System.err.println("FAILED SIZE CHECK");
+// System.err.println("SIZE: " + width + " : " + height);
+// System.err.println("actual: " + frame.getWidth() + " " + frame.getHeight());
+
+ final Dimension size = new Dimension((int) width, (int) height);
+ if (!resizable) {
+ frame.setMinimumSize(size);
+ frame.setMaximumSize(size);
+ panel.setMinimumSize(size);
+ panel.setMaximumSize(size);
+ }
+
+ panel.setPreferredSize(size);
+ frame.setPreferredSize(size);
+
+ if (center) {
+ // same as in screenUtils, but here we set bound instead of just location
+ final Point mouseLocation = MouseInfo.getPointerInfo()
+ .getLocation();
+
+ final GraphicsDevice deviceAtMouse = ScreenUtil.getGraphicsDeviceAt(mouseLocation);
+ final Rectangle bounds = deviceAtMouse.getDefaultConfiguration()
+ .getBounds();
+
+
+ panel.setBounds(bounds.x + (bounds.width / 2) - (int)width / 2,
+ bounds.y + (bounds.height / 2) - (int)height / 2,
+ (int)width,
+ (int)height);
+
+ frame.setBounds(bounds.x + (bounds.width / 2) - (int)width / 2,
+ bounds.y + (bounds.height / 2) - (int)height / 2,
+ (int)width,
+ (int)height);
+ } else {
+ panel.setBounds((int) x,
+ (int) y,
+ (int) width,
+ (int) height);
+ frame.setBounds((int) x,
+ (int) y,
+ (int) width,
+ (int) height);
+ }
+
+ frame.pack();
+ frame.revalidate();
+ frame.repaint();
+
+// System.err.println("recheck SIZE: " + frame.getWidth() + " " + frame.getHeight());
+// System.err.println("recheck SIZE: " + panel.getWidth() + " " + frame.getHeight());
+ }
+ }
+
+ public final
+ void completeShowTransition() {
+ showLatch.countDown();
+ }
+
public
void setTitle(final String title) {
SwingUtil.invokeAndWait(() -> frame.setTitle(title));
@@ -166,24 +261,57 @@ class StageViaSwing {
return frame.getTitle();
}
+
public
void close() {
- SwingUtil.invokeAndWait(frame::dispose);
+ closing = true;
+
+ // "hide" it until we can properly do so.
+ SwingUtil.invokeAndWait(() -> {
+ frame.setOpacity(0F);
+ frame.setBounds(Short.MIN_VALUE, Short.MIN_VALUE, 0, 0);
+ //noinspection deprecation
+ frame.hide();
+ });
+
+ frameDisposer.execute(() -> {
+ // stupid thing flashes on-screen if we run this right away...
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ SwingUtil.invokeLater(frame::dispose);
+ });
+
+ releaseLatch(showAndWaitLatch);
+ }
+
+ private
+ void releaseLatch(final CountDownLatch latch) {
if (inNestedEventLoop) {
- inNestedEventLoop= false;
- com.sun.javafx.tk.Toolkit.getToolkit().exitNestedEventLoop(this, null);
+ inNestedEventLoop = false;
+
+ if (!Platform.isFxApplicationThread()) {
+ JavaFxUtil.invokeAndWait(() -> com.sun.javafx.tk.Toolkit.getToolkit().exitNestedEventLoop(StageViaSwing.this, null));
+ } else {
+ com.sun.javafx.tk.Toolkit.getToolkit().exitNestedEventLoop(StageViaSwing.this, null);
+ }
} else {
- showAndWaitlatch.countDown();
+ latch.countDown();
}
}
public
void setSize(final double width, final double height) {
+ this.width = width;
+ this.height = height;
SwingUtil.invokeAndWait(() -> frame.setSize((int)width, (int)height));
}
public
void setResizable(final boolean resizable) {
+ this.resizable = resizable;
SwingUtil.invokeAndWait(() -> frame.setResizable(resizable));
}
@@ -192,32 +320,69 @@ class StageViaSwing {
SwingUtil.invokeAndWait(() -> frame.setIconImage(icon));
}
+ public
+ void show(final double x, final double y) {
+ // we want to make sure we go BACK to this location when we show the JFRAME on screen
+ this.x = x;
+ this.y = y;
+
+ show();
+ }
+
+ public
+ void showAndWait(final double x, final double y) {
+ // we want to make sure we go BACK to this location when we show the JFRAME on screen
+ this.x = x;
+ this.y = y;
+
+ showAndWait();
+ }
+
public
void show() {
SwingUtil.invokeAndWait(() -> {
- frame.setOpacity(.0f);
frame.setSize(0, 0);
-
frame.setVisible(false);
- frame.setLocation(Short.MIN_VALUE, Short.MIN_VALUE);
+ frame.setOpacity(0f);
+ frame.setBounds(Short.MIN_VALUE, Short.MIN_VALUE, 0, 0);
- // Figure out the size of everything. Because JFXPanel DOES NOT do this.
- frame.pack();
+ frame.revalidate();
+ frame.repaint();
});
- // has javafx stuff on it, must not be called on the EDT
- sizeToScene();
-
- SwingUtil.invokeAndWait(() -> frame.setVisible(true));
+ // Figure out the size of everything. Because JFXPanel DOES NOT do this.
+ // wait until our show animation is complete. There is a small delay out of necessity
// false-positive
//noinspection Duplicates
if (Platform.isFxApplicationThread()) {
inNestedEventLoop = true;
- com.sun.javafx.tk.Toolkit.getToolkit().enterNestedEventLoop(this);
- } else {
+
+ SwingUtil.invokeAndWait(() -> frame.setVisible(true));
+
try {
- showlatch.await();
+ showLatch.await();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ inNestedEventLoop = false;
+
+ sizeToScene();
+
+ SwingUtil.invokeAndWait(() -> {
+ if (showAnimation == null) {
+ opacityProperty.setValue(1F);
+ completeShowTransition();
+ } else {
+ showAnimation.doShow();
+ }
+ });
+ } else {
+ SwingUtil.invokeAndWait(() -> frame.setVisible(true));
+
+ try {
+ showLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
@@ -235,7 +400,7 @@ class StageViaSwing {
com.sun.javafx.tk.Toolkit.getToolkit().enterNestedEventLoop(this);
} else {
try {
- showAndWaitlatch.await();
+ showAndWaitLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
@@ -248,18 +413,12 @@ class StageViaSwing {
JavaFxUtil.invokeAndWait(() -> frame.setModalExclusionType(modal));
}
- public
- void setMinSize(final double width, final double height) {
- SwingUtil.invokeAndWait(() -> frame.setMinimumSize(new Dimension((int)width, (int)height)));
- }
-
public
void sizeToScene() {
SwingUtil.invokeAndWait(() -> {
- frame.invalidate();
- frame.validate();
- }
- );
+ frame.revalidate();
+ frame.repaint();
+ });
// Figure out the size of everything. Because JFXPanel DOES NOT do this.
// must be on the FX app thread
@@ -270,18 +429,20 @@ class StageViaSwing {
// use reflection. This is lame, but necessary. must be on the jfx thread
method.invoke(scene);
- // must be on the EDT
- SwingUtil.invokeAndWait(() -> frame.setSize((int)scene.getWidth(), (int)scene.getHeight()));
+ width = scene.getWidth();
+ height = scene.getHeight();
} catch (InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
});
+
+ SwingUtil.invokeAndWait(this::recheckSize);
}
public
void setScene(final Scene scene) {
// must be on the JFX or EDT threads
- if (!Platform.isFxApplicationThread() || !EventQueue.isDispatchThread()) {
+ if (!Platform.isFxApplicationThread() && !EventQueue.isDispatchThread()) {
JavaFxUtil.invokeAndWait(() -> panel.setScene(scene));
} else {
panel.setScene(scene);
@@ -294,13 +455,26 @@ class StageViaSwing {
}
public
- void setLocation(final double anchorX, final double anchorY) {
- SwingUtil.invokeAndWait(() -> frame.setLocation((int)anchorX, (int)anchorY));
+ void setLocation(final double x, final double y) {
+ // we want to make sure we go BACK to this location when we show the JFRAME on screen
+ if (x != this.x || y != this.y) {
+ this.x = x;
+ this.y = y;
+
+ if (!closing) {
+ SwingUtil.invokeAndWait(() -> frame.setLocation((int)x, (int)y));
+ }
+ }
}
public
- Point getLocation() {
- return frame.getLocation();
+ double getX() {
+ return x;
+ }
+
+ public
+ double getY() {
+ return y;
}
public
diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java b/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java
index e98f9ff..e0b3d72 100644
--- a/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java
+++ b/Dorkbox-Util/src/dorkbox/util/javafx/Wizard.java
@@ -316,7 +316,6 @@ public class Wizard {
borderPane.setCenter(center);
Scene scene = new Scene(borderPane);
- stage.setMinSize(300, 140);
stage.setSize(300, 140);
stage.setScene(scene);
stage.setResizable(false); // hide the minimize/maximize decorations
@@ -328,6 +327,17 @@ public class Wizard {
close();
}
});
+
+ //noinspection Duplicates
+ stage.setShowAnimation(() -> {
+ Timeline timeline = new Timeline();
+ timeline.setCycleCount(1);
+ timeline.getKeyFrames()
+ .addAll(new KeyFrame(Duration.millis(500), new KeyValue(stage.getOpacityProperty(), 1F, Interpolator.EASE_OUT)));
+ // have to trigger that our animation is completed and the show() method may continue
+ timeline.setOnFinished(event -> stage.completeShowTransition());
+ timeline.play();
+ });
}
private
@@ -410,9 +420,7 @@ public class Wizard {
currentPage.ifPresent(pageHistory::push);
currentPage = getFlow().advance(currentPage.orElse(null));
updatePage(stage, true);
-
- }
- );
+ });
}
private
@@ -758,7 +766,10 @@ public class Wizard {
if (!useSpecifiedSize) {
currentPage.anchorPane.autosize();
- stage.sizeToScene();
+
+ if (stage.frame.isShowing()) {
+ stage.sizeToScene();
+ }
}
JavaFxUtil.invokeAndWait(() -> {