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 final javafx.scene.text.Font DEFAULT_FONT = new javafx.scene.text.Font(13);
|
||||||
|
|
||||||
|
|
||||||
public static
|
public static
|
||||||
void showOnSameScreenAsMouseCenter(javafx.stage.Window stage) {
|
void showOnSameScreenAsMouseCenter(javafx.stage.Window stage) {
|
||||||
Point mouseLocation = MouseInfo.getPointerInfo()
|
Point mouseLocation = MouseInfo.getPointerInfo()
|
||||||
|
|
|
@ -17,10 +17,15 @@ package dorkbox.util;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
public
|
public
|
||||||
class SwingUtil {
|
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
|
public static
|
||||||
void showOnSameScreenAsMouseCenter(Container frame) {
|
void showOnSameScreenAsMouseCenter(Container frame) {
|
||||||
Point mouseLocation = MouseInfo.getPointerInfo()
|
Point mouseLocation = MouseInfo.getPointerInfo()
|
||||||
|
|
|
@ -28,7 +28,6 @@ import java.util.Map.Entry;
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final
|
public final
|
||||||
class Sys {
|
class Sys {
|
||||||
public static final int javaVersion = getJavaVersion();
|
|
||||||
public static final boolean isAndroid = getIsAndroid();
|
public static final boolean isAndroid = getIsAndroid();
|
||||||
|
|
||||||
public static final int KILOBYTE = 1024;
|
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
|
public static
|
||||||
void eraseString(String string) {
|
void eraseString(String string) {
|
||||||
|
|
281
Dorkbox-Util/src/dorkbox/util/javafx/Growl.java
Normal file
281
Dorkbox-Util/src/dorkbox/util/javafx/Growl.java
Normal 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
169
Dorkbox-Util/src/dorkbox/util/javafx/GrowlNotification.java
Normal file
169
Dorkbox-Util/src/dorkbox/util/javafx/GrowlNotification.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
467
Dorkbox-Util/src/dorkbox/util/javafx/GrowlPopup.java
Normal file
467
Dorkbox-Util/src/dorkbox/util/javafx/GrowlPopup.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
Dorkbox-Util/src/dorkbox/util/javafx/GrowlPopupViaSwing.java
Normal file
70
Dorkbox-Util/src/dorkbox/util/javafx/GrowlPopupViaSwing.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 com.sun.javafx.application.PlatformImpl;
|
||||||
import dorkbox.util.JavaFxUtil;
|
import dorkbox.util.JavaFxUtil;
|
||||||
|
import dorkbox.util.NamedThreadFactory;
|
||||||
|
import dorkbox.util.ScreenUtil;
|
||||||
import dorkbox.util.SwingUtil;
|
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.application.Platform;
|
||||||
import javafx.beans.value.WritableValue;
|
import javafx.beans.value.WritableValue;
|
||||||
import javafx.embed.swing.JFXPanel;
|
import javafx.embed.swing.JFXPanel;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.event.WindowAdapter;
|
import java.awt.event.*;
|
||||||
import java.awt.event.WindowEvent;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.concurrent.CountDownLatch;
|
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.
|
* 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.
|
* Annoying caveat. All swing setters MUST happen on the EDT.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public
|
public
|
||||||
class StageViaSwing {
|
class StageViaSwing {
|
||||||
|
public static final Executor frameDisposer = Executors.newSingleThreadExecutor(new NamedThreadFactory("Swing Disposer",
|
||||||
|
Thread.MIN_PRIORITY,
|
||||||
|
true));
|
||||||
|
|
||||||
|
|
||||||
final JFrame frame;
|
final JFrame frame;
|
||||||
final JFXPanel panel;
|
final JFXPanel panel;
|
||||||
|
|
||||||
private boolean inNestedEventLoop = false;
|
private volatile boolean inNestedEventLoop = false;
|
||||||
private final CountDownLatch showlatch = new CountDownLatch(1);
|
private final CountDownLatch showLatch = new CountDownLatch(1);
|
||||||
private final CountDownLatch showAndWaitlatch = new CountDownLatch(1);
|
private final CountDownLatch showAndWaitLatch = new CountDownLatch(1);
|
||||||
|
|
||||||
final WritableValue<Float> opacityProperty;
|
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() {
|
StageViaSwing() {
|
||||||
frame = new JFrame() {
|
frame = new JFrame();
|
||||||
|
|
||||||
};
|
|
||||||
panel = new JFXPanel();
|
panel = new JFXPanel();
|
||||||
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
|
|
||||||
|
|
||||||
|
// frame.setLayout(null);
|
||||||
frame.setUndecorated(true);
|
frame.setUndecorated(true);
|
||||||
|
frame.setOpacity(0F);
|
||||||
frame.add(panel);
|
frame.add(panel);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
opacityProperty = new WritableValue<Float>() {
|
opacityProperty = new WritableValue<Float>() {
|
||||||
@Override
|
@Override
|
||||||
public Float getValue() {
|
public Float getValue() {
|
||||||
|
@ -112,50 +135,122 @@ class StageViaSwing {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setValue(Float value) {
|
public void setValue(Float value) {
|
||||||
SwingUtil.invokeLater(() -> frame.setOpacity(value));
|
SwingUtil.invokeAndWait(() -> frame.setOpacity(value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
frame.addWindowListener(new WindowAdapter() {
|
frame.addWindowListener(new WindowAdapter() {
|
||||||
public void windowOpened(WindowEvent e) {
|
public void windowOpened(WindowEvent e) {
|
||||||
Thread thread = new Thread(() -> {
|
if (showAnimation != null) {
|
||||||
try {
|
// Thread thread = new Thread(() -> {
|
||||||
// If this runs now, it will bug out, and flash on the screen before we want it to.
|
// try {
|
||||||
// REALLY dumb, but we have to wait for the system to draw the window and finish BEFORE we move it
|
// If this runs now, it will bug out, and flash on the screen before we want it to.
|
||||||
// otherwise, it'll 'flash' onscreen because it will still be in the middle of it's initial "on-show" animation.
|
// REALLY dumb, but we have to wait for the system to draw the window and finish BEFORE we move it
|
||||||
Thread.sleep(500);
|
// 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) {
|
private
|
||||||
SwingUtil.invokeAndWait(() -> SwingUtil.showOnSameScreenAsMouseCenter(frame));
|
void renderContents() {
|
||||||
}
|
sizeToScene();
|
||||||
|
|
||||||
Timeline timeline = new Timeline();
|
SwingUtil.invokeLater(StageViaSwing.this::recheckSize);
|
||||||
timeline.setCycleCount(1);
|
|
||||||
timeline.getKeyFrames()
|
if (showAnimation == null) {
|
||||||
.addAll(new KeyFrame(Duration.millis(700),
|
opacityProperty.setValue(1F);
|
||||||
new KeyValue(opacityProperty, 1F, Interpolator.EASE_OUT)));
|
completeShowTransition();
|
||||||
timeline.setOnFinished(event -> {
|
} else {
|
||||||
if (inNestedEventLoop) {
|
showAnimation.doShow();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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
|
public
|
||||||
void setTitle(final String title) {
|
void setTitle(final String title) {
|
||||||
SwingUtil.invokeAndWait(() -> frame.setTitle(title));
|
SwingUtil.invokeAndWait(() -> frame.setTitle(title));
|
||||||
|
@ -166,24 +261,57 @@ class StageViaSwing {
|
||||||
return frame.getTitle();
|
return frame.getTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public
|
public
|
||||||
void close() {
|
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) {
|
if (inNestedEventLoop) {
|
||||||
inNestedEventLoop= false;
|
inNestedEventLoop = false;
|
||||||
com.sun.javafx.tk.Toolkit.getToolkit().exitNestedEventLoop(this, null);
|
|
||||||
|
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 {
|
} else {
|
||||||
showAndWaitlatch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
public
|
||||||
void setSize(final double width, final double height) {
|
void setSize(final double width, final double height) {
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
SwingUtil.invokeAndWait(() -> frame.setSize((int)width, (int)height));
|
SwingUtil.invokeAndWait(() -> frame.setSize((int)width, (int)height));
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
public
|
||||||
void setResizable(final boolean resizable) {
|
void setResizable(final boolean resizable) {
|
||||||
|
this.resizable = resizable;
|
||||||
SwingUtil.invokeAndWait(() -> frame.setResizable(resizable));
|
SwingUtil.invokeAndWait(() -> frame.setResizable(resizable));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,32 +320,69 @@ class StageViaSwing {
|
||||||
SwingUtil.invokeAndWait(() -> frame.setIconImage(icon));
|
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
|
public
|
||||||
void show() {
|
void show() {
|
||||||
SwingUtil.invokeAndWait(() -> {
|
SwingUtil.invokeAndWait(() -> {
|
||||||
frame.setOpacity(.0f);
|
|
||||||
frame.setSize(0, 0);
|
frame.setSize(0, 0);
|
||||||
|
|
||||||
frame.setVisible(false);
|
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.revalidate();
|
||||||
frame.pack();
|
frame.repaint();
|
||||||
});
|
});
|
||||||
|
|
||||||
// has javafx stuff on it, must not be called on the EDT
|
// Figure out the size of everything. Because JFXPanel DOES NOT do this.
|
||||||
sizeToScene();
|
|
||||||
|
|
||||||
SwingUtil.invokeAndWait(() -> frame.setVisible(true));
|
|
||||||
|
|
||||||
|
// wait until our show animation is complete. There is a small delay out of necessity
|
||||||
// false-positive
|
// false-positive
|
||||||
//noinspection Duplicates
|
//noinspection Duplicates
|
||||||
if (Platform.isFxApplicationThread()) {
|
if (Platform.isFxApplicationThread()) {
|
||||||
inNestedEventLoop = true;
|
inNestedEventLoop = true;
|
||||||
com.sun.javafx.tk.Toolkit.getToolkit().enterNestedEventLoop(this);
|
|
||||||
} else {
|
SwingUtil.invokeAndWait(() -> frame.setVisible(true));
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (InterruptedException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
@ -235,7 +400,7 @@ class StageViaSwing {
|
||||||
com.sun.javafx.tk.Toolkit.getToolkit().enterNestedEventLoop(this);
|
com.sun.javafx.tk.Toolkit.getToolkit().enterNestedEventLoop(this);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
showAndWaitlatch.await();
|
showAndWaitLatch.await();
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
@ -248,18 +413,12 @@ class StageViaSwing {
|
||||||
JavaFxUtil.invokeAndWait(() -> frame.setModalExclusionType(modal));
|
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
|
public
|
||||||
void sizeToScene() {
|
void sizeToScene() {
|
||||||
SwingUtil.invokeAndWait(() -> {
|
SwingUtil.invokeAndWait(() -> {
|
||||||
frame.invalidate();
|
frame.revalidate();
|
||||||
frame.validate();
|
frame.repaint();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Figure out the size of everything. Because JFXPanel DOES NOT do this.
|
// Figure out the size of everything. Because JFXPanel DOES NOT do this.
|
||||||
// must be on the FX app thread
|
// 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
|
// use reflection. This is lame, but necessary. must be on the jfx thread
|
||||||
method.invoke(scene);
|
method.invoke(scene);
|
||||||
|
|
||||||
// must be on the EDT
|
width = scene.getWidth();
|
||||||
SwingUtil.invokeAndWait(() -> frame.setSize((int)scene.getWidth(), (int)scene.getHeight()));
|
height = scene.getHeight();
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
SwingUtil.invokeAndWait(this::recheckSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
public
|
||||||
void setScene(final Scene scene) {
|
void setScene(final Scene scene) {
|
||||||
// must be on the JFX or EDT threads
|
// must be on the JFX or EDT threads
|
||||||
if (!Platform.isFxApplicationThread() || !EventQueue.isDispatchThread()) {
|
if (!Platform.isFxApplicationThread() && !EventQueue.isDispatchThread()) {
|
||||||
JavaFxUtil.invokeAndWait(() -> panel.setScene(scene));
|
JavaFxUtil.invokeAndWait(() -> panel.setScene(scene));
|
||||||
} else {
|
} else {
|
||||||
panel.setScene(scene);
|
panel.setScene(scene);
|
||||||
|
@ -294,13 +455,26 @@ class StageViaSwing {
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
public
|
||||||
void setLocation(final double anchorX, final double anchorY) {
|
void setLocation(final double x, final double y) {
|
||||||
SwingUtil.invokeAndWait(() -> frame.setLocation((int)anchorX, (int)anchorY));
|
// 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
|
public
|
||||||
Point getLocation() {
|
double getX() {
|
||||||
return frame.getLocation();
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public
|
||||||
|
double getY() {
|
||||||
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
public
|
public
|
||||||
|
|
|
@ -316,7 +316,6 @@ public class Wizard {
|
||||||
borderPane.setCenter(center);
|
borderPane.setCenter(center);
|
||||||
|
|
||||||
Scene scene = new Scene(borderPane);
|
Scene scene = new Scene(borderPane);
|
||||||
stage.setMinSize(300, 140);
|
|
||||||
stage.setSize(300, 140);
|
stage.setSize(300, 140);
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
stage.setResizable(false); // hide the minimize/maximize decorations
|
stage.setResizable(false); // hide the minimize/maximize decorations
|
||||||
|
@ -328,6 +327,17 @@ public class Wizard {
|
||||||
close();
|
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
|
private
|
||||||
|
@ -410,9 +420,7 @@ public class Wizard {
|
||||||
currentPage.ifPresent(pageHistory::push);
|
currentPage.ifPresent(pageHistory::push);
|
||||||
currentPage = getFlow().advance(currentPage.orElse(null));
|
currentPage = getFlow().advance(currentPage.orElse(null));
|
||||||
updatePage(stage, true);
|
updatePage(stage, true);
|
||||||
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -758,7 +766,10 @@ public class Wizard {
|
||||||
|
|
||||||
if (!useSpecifiedSize) {
|
if (!useSpecifiedSize) {
|
||||||
currentPage.anchorPane.autosize();
|
currentPage.anchorPane.autosize();
|
||||||
stage.sizeToScene();
|
|
||||||
|
if (stage.frame.isShowing()) {
|
||||||
|
stage.sizeToScene();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
JavaFxUtil.invokeAndWait(() -> {
|
JavaFxUtil.invokeAndWait(() -> {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user