diff --git a/Dorkbox-Util/src/dorkbox/util/JavaFxUtil.java b/Dorkbox-Util/src/dorkbox/util/JavaFxUtil.java index 48a3d60..08d7530 100644 --- a/Dorkbox-Util/src/dorkbox/util/JavaFxUtil.java +++ b/Dorkbox-Util/src/dorkbox/util/JavaFxUtil.java @@ -15,6 +15,7 @@ class JavaFxUtil { public static final javafx.scene.text.Font DEFAULT_FONT = new javafx.scene.text.Font(13); + public static void showOnSameScreenAsMouseCenter(javafx.stage.Window stage) { Point mouseLocation = MouseInfo.getPointerInfo() diff --git a/Dorkbox-Util/src/dorkbox/util/SwingUtil.java b/Dorkbox-Util/src/dorkbox/util/SwingUtil.java index 31ddcfa..9fec0eb 100644 --- a/Dorkbox-Util/src/dorkbox/util/SwingUtil.java +++ b/Dorkbox-Util/src/dorkbox/util/SwingUtil.java @@ -17,10 +17,15 @@ package dorkbox.util; import javax.swing.*; import java.awt.*; +import java.awt.image.BufferedImage; import java.lang.reflect.InvocationTargetException; public class SwingUtil { + + /** used when setting various icon components in the GUI to "nothing", since null doesn't work */ + public static final Image BLANK_ICON = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB_PRE); + public static void showOnSameScreenAsMouseCenter(Container frame) { Point mouseLocation = MouseInfo.getPointerInfo() diff --git a/Dorkbox-Util/src/dorkbox/util/Sys.java b/Dorkbox-Util/src/dorkbox/util/Sys.java index 4e013f0..ad9d9d4 100644 --- a/Dorkbox-Util/src/dorkbox/util/Sys.java +++ b/Dorkbox-Util/src/dorkbox/util/Sys.java @@ -28,7 +28,6 @@ import java.util.Map.Entry; @SuppressWarnings("unused") public final class Sys { - public static final int javaVersion = getJavaVersion(); public static final boolean isAndroid = getIsAndroid(); public static final int KILOBYTE = 1024; @@ -57,38 +56,6 @@ class Sys { } } - private static - int getJavaVersion() { - String fullJavaVersion = System.getProperty("java.version"); - - // Converts a java version string, such as "1.7u45", and converts it into 7 - char versionChar; - if (fullJavaVersion.startsWith("1.")) { - versionChar = fullJavaVersion.charAt(2); - } - else { - versionChar = fullJavaVersion.charAt(0); - } - - switch (versionChar) { - case '4': - return 4; - case '5': - return 5; - case '6': - return 6; - case '7': - return 7; - case '8': - return 8; - case '9': - return 9; - default: - return -1; - } - } - - public static void eraseString(String string) { diff --git a/Dorkbox-Util/src/dorkbox/util/javafx/Growl.java b/Dorkbox-Util/src/dorkbox/util/javafx/Growl.java new file mode 100644 index 0000000..0b9c0a6 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/javafx/Growl.java @@ -0,0 +1,281 @@ +/** + * 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 com.sun.javafx.application.PlatformImpl; +import dorkbox.util.JavaFxUtil; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.image.ImageView; +import javafx.stage.Window; +import javafx.util.Duration; +import org.controlsfx.tools.Utils; + +import java.util.ArrayList; +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 Growl { + + /*************************************************************************** + * * Static fields * * + **************************************************************************/ + + private static final String STYLE_CLASS_DARK = "dark"; //$NON-NLS-1$ + + /*************************************************************************** + * * Private fields * * + **************************************************************************/ + + String title; + String text; + Node graphic; + + Pos position = Pos.BOTTOM_RIGHT; + private Duration hideAfterDuration = Duration.seconds(5); + boolean hideCloseButton; + private EventHandler onAction; + Window owner; + + 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 + Growl() { + // no-op + } + + /*************************************************************************** + * * Public API * * + **************************************************************************/ + + /** + * Call this to begin the process of building a notification to show. + */ + public static + Growl create() { + // make sure that javafx application thread is started + // Note that calling PlatformImpl.startup more than once is OK + PlatformImpl.startup(() -> { + // No need to do anything here + }); + + return new Growl(); + } + + /** + * Specify the text to show in the notification. + */ + public + Growl text(String text) { + this.text = text; + return this; + } + + /** + * Specify the title to show in the notification. + */ + public + Growl title(String title) { + this.title = title; + return this; + } + + /** + * Specify the graphic to show in the notification. + */ + public + Growl 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 + Growl 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 + Growl 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 + Growl 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 + Growl 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 + 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(() -> {