Reworked SwingViaStage, reworked growl, tweaked show/hide behavior
This commit is contained in:
parent
1607d0a087
commit
7aa799a805
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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(() -> {
|
||||
|
|
Loading…
Reference in New Issue