Reworked SwingViaStage, reworked growl, tweaked show/hide behavior

This commit is contained in:
nathan 2015-08-21 17:10:25 +02:00
parent 1607d0a087
commit 7aa799a805
10 changed files with 1264 additions and 750 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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) {

View File

@ -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.
*
* <h3>Screenshot</h3>
* <p>
* The following screenshot shows a sample notification rising from the
* bottom-right corner of my screen:
*
* <br/>
* <br/>
* <img src="notifications.png"/>
*
* <h3>Code Example:</h3>
* <p>
* To create the notification shown in the screenshot, simply do the following:
*
* <pre>
* {@code
* Notifications.create()
* .title("Title Text")
* .text("Hello World 0!")
* .showWarning();
* }
* </pre>
*/
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<ActionEvent> onAction;
Window owner;
List<String> 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<ActionEvent> 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());
}
}

View File

@ -0,0 +1,169 @@
/**
* Copyright (c) 2014, ControlsFX All rights reserved.
* <p>
* 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.
* <p>
* 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.
* <p>
* MODIFIED BY DORKBOX, LLC Copyright 2015 dorkbox, llc
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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));
}
}

View File

@ -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<GrowlPopup> 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<String> 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<KeyFrame> 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;
}
}
}

View File

@ -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;
}
}

View File

@ -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.
*
* <h3>Screenshot</h3>
* <p>
* The following screenshot shows a sample notification rising from the
* bottom-right corner of my screen:
*
* <br/>
* <br/>
* <img src="notifications.png"/>
*
* <h3>Code Example:</h3>
* <p>
* To create the notification shown in the screenshot, simply do the following:
*
* <pre>
* {@code
* Notifications.create()
* .title("Title Text")
* .text("Hello World 0!")
* .showWarning();
* }
* </pre>
*/
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<Action> actions = FXCollections.observableArrayList();
private Pos position = Pos.BOTTOM_RIGHT;
private Duration hideAfterDuration = Duration.seconds(5);
private boolean hideCloseButton;
private EventHandler<ActionEvent> onAction;
private Window owner;
private List<String> 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<ActionEvent> 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.<Action> 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<Pos, List<Popup>> 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<Action> 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<ActionEvent>() {
@Override
public void handle(ActionEvent e) {
hide(popup, p);
}
});
return timeline;
}
private void addPopupToMap(Pos p, Popup popup) {
List<Popup> 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<Popup> popups = popupsMap.get(p);
popups.remove(popup);
}
}
private void doAnimation(Pos p, Popup changedPopup) {
List<Popup> 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;
}
}
}
}

View File

@ -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<Float> 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<Float>() {
@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

View File

@ -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(() -> {