View Javadoc
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   }