1 /*
2 * *************************************************************************************************************************************************************
3 *
4 * SteelBlue: DCI User Interfaces
5 * http://tidalwave.it/projects/steelblue
6 *
7 * Copyright (C) 2015 - 2025 by Tidalwave s.a.s. (http://tidalwave.it)
8 *
9 * *************************************************************************************************************************************************************
10 *
11 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
12 * You may obtain a copy of the License at
13 *
14 * http://www.apache.org/licenses/LICENSE-2.0
15 *
16 * 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
17 * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
18 *
19 * *************************************************************************************************************************************************************
20 *
21 * git clone https://bitbucket.org/tidalwave/steelblue-src
22 * git clone https://github.com/tidalwave-it/steelblue-src
23 *
24 * *************************************************************************************************************************************************************
25 */
26 package it.tidalwave.ui.javafx.impl;
27
28 import jakarta.annotation.Nonnull;
29 import javafx.animation.KeyFrame;
30 import javafx.animation.Timeline;
31 import javafx.scene.Parent;
32 import javafx.scene.Scene;
33 import javafx.scene.input.KeyCombination;
34 import javafx.stage.Screen;
35 import javafx.stage.Stage;
36 import javafx.stage.StageStyle;
37 import javafx.util.Duration;
38 import javafx.application.Application;
39 import javafx.application.Platform;
40 import it.tidalwave.ui.javafx.NodeAndDelegate;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import it.tidalwave.util.Key;
44 import it.tidalwave.util.PreferencesHandler;
45 import lombok.Getter;
46 import lombok.Setter;
47
48 /***************************************************************************************************************************************************************
49 *
50 * @author Fabrizio Giudici
51 *
52 **************************************************************************************************************************************************************/
53 public abstract class JavaFXApplicationWithSplash extends Application
54 {
55 private static final String K_BASE_NAME = "it.tidalwave.ui.javafx";
56
57 /** A property representing the initial main window size as a percentual of the screen size. */
58 public static final Key<Double> K_INITIAL_SIZE = Key.of(K_BASE_NAME + ".initialSize", Double.class);
59
60 /** Whether the application should start maximized. */
61 public static final Key<Boolean> K_MAXIMIZED = Key.of(K_BASE_NAME + ".maximized", Boolean.class);
62
63 /** Whether the application should start at full screen. */
64 public static final Key<Boolean> K_FULL_SCREEN = Key.of(K_BASE_NAME + ".fullScreen", Boolean.class);
65
66 /** Whether the application should always stay at full screen. */
67 public static final Key<Boolean> K_FULL_SCREEN_LOCKED = Key.of(K_BASE_NAME + ".fullScreenLocked", Boolean.class);
68
69 /** The minimum duration of the splash screen. */
70 public static final Key<Duration> K_MIN_SPLASH_DURATION = Key.of(K_BASE_NAME + ".minSplashDuration", Duration.class);
71
72 /** Whether invocations to presentation delegate methods should be logged at debug level. */
73 public static final Key<Boolean> K_LOG_DELEGATE_INVOCATIONS = Key.of(K_BASE_NAME + ".logDelegateInvocations", Boolean.class);
74
75 private static final String DEFAULT_APPLICATION_FXML = "Application.fxml";
76
77 private static final String DEFAULT_SPLASH_FXML = "Splash.fxml";
78
79 private static final Duration DEFAULT_MIN_SPLASH_DURATION = Duration.seconds(2);
80
81 // Don't use Slf4j and its static logger - give Main a chance to initialize things
82 private final Logger log = LoggerFactory.getLogger(JavaFXApplicationWithSplash.class);
83
84 private Splash splash;
85
86 private boolean maximized;
87
88 private boolean fullScreen;
89
90 private boolean fullScreenLocked;
91
92 @Getter @Setter
93 protected String applicationFxml = DEFAULT_APPLICATION_FXML;
94
95 @Getter @Setter
96 protected String splashFxml = DEFAULT_SPLASH_FXML;
97
98 /***********************************************************************************************************************************************************
99 * {@inheritDoc}
100 **********************************************************************************************************************************************************/
101 @Override
102 public void start (@Nonnull final Stage stage)
103 {
104 log.info("start({})", stage);
105 splash = new Splash(this, splashFxml, this::createScene);
106 splash.init();
107 final var preferencesHandler = PreferencesHandler.getInstance();
108 fullScreen = preferencesHandler.getProperty(K_FULL_SCREEN).orElse(false);
109 fullScreenLocked = preferencesHandler.getProperty(K_FULL_SCREEN_LOCKED).orElse(false);
110 maximized = preferencesHandler.getProperty(K_MAXIMIZED).orElse(false);
111
112 final var splashStage = new Stage(StageStyle.TRANSPARENT);
113 stage.setMaximized(maximized);
114 // splashStage.setMaximized(maximized); FIXME: doesn't work
115 configureFullScreen(stage);
116 // configureFullScreen(splashStage); FIXME: deadlocks JDK 1.8.0_40
117
118 if (!maximized && !fullScreen)
119 {
120 splashStage.centerOnScreen();
121 }
122
123 final var splashCreationTime = System.currentTimeMillis();
124 splash.show(splashStage);
125 // No executors available here
126 final var thread = new Thread(() -> initializeInBackground(stage, splashStage, splashCreationTime), "initializer");
127 thread.start();
128 }
129
130 /***********************************************************************************************************************************************************
131 *
132 **********************************************************************************************************************************************************/
133 protected void onStageCreated (@Nonnull final Stage stage, @Nonnull final NodeAndDelegate<?> applicationNad)
134 {
135 }
136
137 /***********************************************************************************************************************************************************
138 *
139 **********************************************************************************************************************************************************/
140 @Nonnull
141 protected abstract NodeAndDelegate<?> createParent();
142
143 /***********************************************************************************************************************************************************
144 *
145 **********************************************************************************************************************************************************/
146 protected abstract void initializeInBackground();
147
148 /***********************************************************************************************************************************************************
149 * Invoked when the main {@link Stage} is being closed. This method is called in the JavaFX thread.
150 **********************************************************************************************************************************************************/
151 protected void onCloseRequest()
152 {
153 }
154
155 /***********************************************************************************************************************************************************
156 *
157 **********************************************************************************************************************************************************/
158 @Nonnull
159 protected Scene createScene (@Nonnull final Parent parent)
160 {
161 return new Scene(parent);
162 }
163
164 /***********************************************************************************************************************************************************
165 * Calls {@link #initializeInBackground()} and then {@link #afterInitializationCompleted(Stage, Stage, long)} in the JavaFX thread.
166 * @param stage the main {@code Stage}
167 * @param splashStage the splash {@code Stage}
168 * @param splashCreationTime the time at which the splash {@code Stage} was created
169 **********************************************************************************************************************************************************/
170 private void initializeInBackground (@Nonnull final Stage stage, @Nonnull final Stage splashStage, final long splashCreationTime)
171 {
172 try
173 {
174 initializeInBackground();
175 Platform.runLater(() -> afterInitializationCompleted(stage, splashStage, splashCreationTime));
176 }
177 catch (Throwable t)
178 {
179 log.error("", t);
180 }
181 }
182
183 /***********************************************************************************************************************************************************
184 * Called after the initialisation in background has been completed, this method dismisses the splash {@link Stage} and brings the main {@code Stage} to
185 * view.
186 * @param stage the main {@code Stage}
187 * @param splashStage the splash {@code Stage}
188 * @param splashCreationTime the time at which the splash {@code Stage} was created
189 **********************************************************************************************************************************************************/
190 private void afterInitializationCompleted (@Nonnull final Stage stage, @Nonnull final Stage splashStage, final long splashCreationTime)
191 {
192 log.info("afterInitializationCompleted({}, {}, {})", stage, splashStage, splashCreationTime);
193 final var preferencesHandler = PreferencesHandler.getInstance();
194 final var applicationNad = createParent();
195 final var scene = createScene((Parent)applicationNad.getNode());
196 stage.setOnCloseRequest(event -> onCloseRequest());
197 stage.setScene(scene);
198 onStageCreated(stage, applicationNad);
199 final double scale = preferencesHandler.getProperty(K_INITIAL_SIZE).orElse(0.65);
200 final var screenSize = Screen.getPrimary().getBounds();
201 stage.setWidth(scale * screenSize.getWidth());
202 stage.setHeight(scale * screenSize.getHeight());
203 stage.show();
204 splashStage.toFront();
205
206 final var duration = preferencesHandler.getProperty(K_MIN_SPLASH_DURATION).orElse(DEFAULT_MIN_SPLASH_DURATION);
207 final var delay = Math.max(0, splashCreationTime + duration.toMillis() - System.currentTimeMillis());
208 final var dismissSplash = new Timeline(new KeyFrame(Duration.millis(delay), e -> splash.dismiss()));
209 dismissSplash.play();
210 }
211
212 /***********************************************************************************************************************************************************
213 *
214 **********************************************************************************************************************************************************/
215 private void configureFullScreen (@Nonnull final Stage stage)
216 {
217 stage.setFullScreen(fullScreen);
218
219 if (fullScreen && fullScreenLocked)
220 {
221 stage.setFullScreenExitHint("");
222 stage.setFullScreenExitKeyCombination(KeyCombination.NO_MATCH);
223 }
224 }
225 }