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 }