feat(ui): show a splash screen while the desktop interface starts
SplashPreloader is a JavaFX Preloader, registered by GuiBootstrap through javafx.preloader, so it only exists on the --ui path and shows as soon as the toolkit is up, before the Spring context is built. It displays the splash image with the application name, the build version read from META-INF/build-info.properties, a status line and a progress bar, and fades out once the main window is actually on screen. StartupProgressReporter, a BeanPostProcessor added to the context before it refreshes, turns the context's construction into real progress (beans initialised over bean definitions) and status lines following the measured phases: opening the library, loading components, preparing the interface. The template's placeholder custom.property is dropped from build-info. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
This commit is contained in:
@@ -488,15 +488,11 @@
|
||||
</requiresUnpack>
|
||||
</configuration>
|
||||
<executions>
|
||||
<!-- META-INF/build-info.properties: build.version is shown on the splash screen (SplashPreloader). -->
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build-info</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<additionalProperties>
|
||||
<custom.property>your-value</custom.property>
|
||||
</additionalProperties>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.icroco.pholio.ui;
|
||||
|
||||
import javafx.application.Application;
|
||||
import org.icroco.pholio.ui.splash.SplashPreloader;
|
||||
|
||||
/**
|
||||
* Entry point for the desktop launch path.
|
||||
@@ -46,6 +47,13 @@ public final class GuiBootstrap {
|
||||
*/
|
||||
private static final String DISABLE_PRISM_DIRTY_OPTS_PROPERTY = "prism.dirtyopts";
|
||||
|
||||
/**
|
||||
* The splash screen: {@link Application#launch} reads this property to start a JavaFX preloader before
|
||||
* the application's own {@code init()}. Set here, on the desktop path only, so a headless run never shows
|
||||
* it; an explicit value (an empty one to disable it) is left alone.
|
||||
*/
|
||||
private static final String PRELOADER_PROPERTY = "javafx.preloader";
|
||||
|
||||
private GuiBootstrap() {
|
||||
}
|
||||
|
||||
@@ -72,5 +80,8 @@ public final class GuiBootstrap {
|
||||
if (System.getProperty(DISABLE_PRISM_DIRTY_OPTS_PROPERTY) == null) {
|
||||
System.setProperty(DISABLE_PRISM_DIRTY_OPTS_PROPERTY, "false");
|
||||
}
|
||||
if (System.getProperty(PRELOADER_PROPERTY) == null) {
|
||||
System.setProperty(PRELOADER_PROPERTY, SplashPreloader.class.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import javafx.application.HostServices;
|
||||
import javafx.stage.Stage;
|
||||
import org.icroco.pholio.LaunchMode;
|
||||
import org.icroco.pholio.PholioBootstrap;
|
||||
import org.icroco.pholio.ui.splash.SplashNotification;
|
||||
import org.icroco.pholio.ui.splash.StartupProgressReporter;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -37,15 +39,29 @@ public class PholioFxApplication extends Application {
|
||||
|
||||
private @Nullable ConfigurableApplicationContext context;
|
||||
|
||||
/** Feeds {@code SplashPreloader} while the context builds — see {@link StartupProgressReporter}. */
|
||||
private final StartupProgressReporter startupProgress = new StartupProgressReporter(this::notifyPreloader);
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
String[] args = getParameters().getRaw().toArray(String[]::new);
|
||||
context = PholioBootstrap.builder(LaunchMode.UI)
|
||||
.initializers(hostServicesBean())
|
||||
.initializers(hostServicesBean(), splashProgress())
|
||||
.run(args);
|
||||
log.info("Spring context ready");
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks {@link #startupProgress} into the context before it refreshes: as a bean post-processor, to see
|
||||
* every bean being built, and as a factory post-processor, to learn how many there are once scanned.
|
||||
*/
|
||||
private ApplicationContextInitializer<ConfigurableApplicationContext> splashProgress() {
|
||||
return ctx -> {
|
||||
ctx.addBeanFactoryPostProcessor(factory -> startupProgress.expect(factory.getBeanDefinitionCount()));
|
||||
ctx.getBeanFactory().addBeanPostProcessor(startupProgress);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes {@link HostServices} as a bean.
|
||||
*
|
||||
@@ -59,10 +75,13 @@ public class PholioFxApplication extends Application {
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
startupProgress.interfaceStarted();
|
||||
MacOsProcessName.set("Pholio");
|
||||
setDockIcon();
|
||||
// JavaFX always calls init() before start(), so context is set by the time this runs.
|
||||
Objects.requireNonNull(context).getBean(StageManager.class).showMainWindow(primaryStage);
|
||||
// Only now that the main window is actually on screen: the splash fades out, never leaving a gap.
|
||||
notifyPreloader(SplashNotification.finished());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.icroco.pholio.ui.splash;
|
||||
|
||||
import javafx.application.Preloader.PreloaderNotification;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* What the application tells {@link SplashPreloader} while it starts.
|
||||
*
|
||||
* @param progress 0 to 1, or a negative value to leave the progress bar where it is
|
||||
* @param message already-translated status line, or {@code null} to keep the current one
|
||||
* @param done the main window is showing: fade the splash out
|
||||
*/
|
||||
public record SplashNotification(double progress, @Nullable String message, boolean done) implements PreloaderNotification {
|
||||
|
||||
public static SplashNotification status(double progress, @Nullable String message) {
|
||||
return new SplashNotification(progress, message, false);
|
||||
}
|
||||
|
||||
public static SplashNotification finished() {
|
||||
return new SplashNotification(1, null, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package org.icroco.pholio.ui.splash;
|
||||
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.application.Preloader;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.util.Duration;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Properties;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* The splash screen of a {@code --ui} launch — a JavaFX {@link Preloader}, so it exists only on the desktop
|
||||
* path ({@code GuiBootstrap} names it through the {@code javafx.preloader} property) and shows as soon as the
|
||||
* JavaFX toolkit is up: before {@code PholioFxApplication.init()}, which is where the Spring context — the
|
||||
* bulk of the startup time — gets built.
|
||||
*
|
||||
* <p>It stays until the application says the main window is showing ({@link SplashNotification#done()},
|
||||
* sent from {@code PholioFxApplication.start}), then fades out. JavaFX's own lifecycle notifications are
|
||||
* deliberately ignored: {@code BEFORE_START} arrives before the main window is built, which would leave a
|
||||
* gap with nothing on screen. Progress and status come from {@link StartupProgressReporter}.
|
||||
*
|
||||
* <p>Built with no Spring and none of the application's stylesheets — neither exists yet — so everything
|
||||
* here is plain JavaFX, styled inline plus a small embedded stylesheet for the progress bar, and the first
|
||||
* status line comes straight from the {@code messages} bundle in the JVM's default locale (later lines are
|
||||
* translated by the application itself).
|
||||
*/
|
||||
public class SplashPreloader extends Preloader {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SplashPreloader.class);
|
||||
|
||||
/** Half the image's own 1376×768: crisp on HiDPI screens, a reasonable size on the others. */
|
||||
private static final double WIDTH = 688;
|
||||
private static final double HEIGHT = 384;
|
||||
|
||||
private static final String PROGRESS_CSS = """
|
||||
.splash-progress { -fx-background-color: transparent; -fx-padding: 0; -fx-pref-height: 4; }
|
||||
.splash-progress > .track { -fx-background-color: rgba(255,255,255,0.18); -fx-background-insets: 0;
|
||||
-fx-background-radius: 2; }
|
||||
.splash-progress > .bar { -fx-background-color: #7fd4c9; -fx-background-insets: 0;
|
||||
-fx-background-radius: 2; -fx-padding: 2; }
|
||||
""";
|
||||
|
||||
private final Label status = new Label();
|
||||
private final ProgressBar progress = new ProgressBar(ProgressBar.INDETERMINATE_PROGRESS);
|
||||
|
||||
private @Nullable Stage stage;
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
this.stage = stage;
|
||||
|
||||
ImageView image = new ImageView(new Image(SplashPreloader.class.getResourceAsStream("/images/splashscreen.png")));
|
||||
image.setFitWidth(WIDTH);
|
||||
image.setFitHeight(HEIGHT);
|
||||
image.setSmooth(true);
|
||||
|
||||
Label title = new Label("Pholio");
|
||||
title.setStyle("-fx-text-fill: white; -fx-font-size: 26px; -fx-font-weight: bold;"
|
||||
+ " -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.7), 8, 0.2, 0, 1);");
|
||||
Label version = new Label(buildVersion());
|
||||
version.setStyle("-fx-text-fill: rgba(255,255,255,0.75); -fx-font-size: 13px;"
|
||||
+ " -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.7), 6, 0.2, 0, 1);");
|
||||
version.setVisible(!version.getText().isEmpty());
|
||||
HBox heading = new HBox(10, title, version);
|
||||
heading.setAlignment(Pos.BASELINE_LEFT);
|
||||
status.setText(bundleText("splash.starting"));
|
||||
status.setStyle("-fx-text-fill: rgba(255,255,255,0.85); -fx-font-size: 12px;"
|
||||
+ " -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.8), 6, 0.3, 0, 1);");
|
||||
progress.setPrefWidth(WIDTH - 48);
|
||||
progress.getStyleClass().add("splash-progress");
|
||||
|
||||
VBox overlay = new VBox(6, heading, status, progress);
|
||||
overlay.setAlignment(Pos.BOTTOM_LEFT);
|
||||
overlay.setPadding(new Insets(0, 24, 20, 24));
|
||||
overlay.setStyle("-fx-background-color: linear-gradient(to bottom, transparent 55%, rgba(0,0,0,0.65) 100%);");
|
||||
|
||||
StackPane root = new StackPane(image, overlay);
|
||||
Rectangle clip = new Rectangle(WIDTH, HEIGHT);
|
||||
clip.setArcWidth(24);
|
||||
clip.setArcHeight(24);
|
||||
root.setClip(clip);
|
||||
|
||||
Scene scene = new Scene(root, WIDTH, HEIGHT);
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
// The track and the bar are the ProgressBar's own sub-nodes, out of reach of an inline style.
|
||||
scene.getStylesheets().add("data:text/css," + URLEncoder.encode(PROGRESS_CSS, StandardCharsets.UTF_8).replace("+", "%20"));
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
stage.setTitle("Pholio");
|
||||
Rectangle2D screen = Screen.getPrimary().getVisualBounds();
|
||||
stage.setX(screen.getMinX() + (screen.getWidth() - WIDTH) / 2);
|
||||
stage.setY(screen.getMinY() + (screen.getHeight() - HEIGHT) / 2);
|
||||
stage.show();
|
||||
log.debug("Splash screen shown");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleApplicationNotification(PreloaderNotification notification) {
|
||||
if (!(notification instanceof SplashNotification splash)) {
|
||||
return;
|
||||
}
|
||||
if (splash.message() != null) {
|
||||
status.setText(splash.message());
|
||||
}
|
||||
if (splash.progress() >= 0) {
|
||||
progress.setProgress(splash.progress());
|
||||
}
|
||||
if (splash.done()) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
/** See the class javadoc: the application decides when the splash goes, not JavaFX's lifecycle. */
|
||||
@Override
|
||||
public void handleStateChangeNotification(StateChangeNotification notification) {
|
||||
}
|
||||
|
||||
/** A failed startup must not leave the splash covering JavaFX's own error reporting. */
|
||||
@Override
|
||||
public boolean handleErrorNotification(ErrorNotification notification) {
|
||||
if (stage != null) {
|
||||
stage.hide();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void close() {
|
||||
Stage shown = stage;
|
||||
if (shown == null || !shown.isShowing()) {
|
||||
return;
|
||||
}
|
||||
FadeTransition fade = new FadeTransition(Duration.millis(250), shown.getScene().getRoot());
|
||||
fade.setToValue(0);
|
||||
fade.setOnFinished(event -> shown.hide());
|
||||
fade.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code build.version} from the {@code META-INF/build-info.properties} spring-boot-maven-plugin
|
||||
* generates at build time — empty when the classes were compiled without it (an IDE-only build).
|
||||
*/
|
||||
private static String buildVersion() {
|
||||
try (InputStream in = SplashPreloader.class.getResourceAsStream("/META-INF/build-info.properties")) {
|
||||
if (in == null) {
|
||||
return "";
|
||||
}
|
||||
Properties info = new Properties();
|
||||
info.load(in);
|
||||
return info.getProperty("build.version", "");
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.debug("Could not read build-info.properties", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String bundleText(String key) {
|
||||
try {
|
||||
return ResourceBundle.getBundle("messages", Locale.getDefault()).getString(key);
|
||||
}
|
||||
catch (MissingResourceException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package org.icroco.pholio.ui.splash;
|
||||
|
||||
import org.icroco.pholio.infra.i18n.I18nService;
|
||||
import org.icroco.pholio.infra.library.LibraryService;
|
||||
import org.icroco.pholio.infra.preferences.AppPreferences;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Turns the Spring context's construction — most of a desktop launch — into splash-screen progress.
|
||||
*
|
||||
* <p>Progress is real, not a timer: the share of the context's bean definitions initialised so far, mapped
|
||||
* onto {@link #CONTEXT_START}..{@link #CONTEXT_END} of the bar (the rest belongs to JavaFX starting before,
|
||||
* and to the main window being built after). Not every definition is instantiated at startup, so the bar
|
||||
* rarely reaches {@link #CONTEXT_END} on its own — {@link #interfaceStarted()} and the final notification
|
||||
* complete it.
|
||||
*
|
||||
* <p>The status line follows the phases measured on a real launch, in order: opening the library (its
|
||||
* database, and Flyway migrations if any — spotted as {@link LibraryService} being built, which a real launch
|
||||
* logs just before the library's datasource opens; not the {@code DataSource} itself, which the {@code ui}
|
||||
* layer must not depend on), then loading the application's components, then building the interface (the
|
||||
* first bean from the {@code ui} package). Lines are translated through {@link I18nService} once it exists — early
|
||||
* enough for every line after the first — and each phase is announced once.
|
||||
*
|
||||
* <p>Registered by {@code PholioFxApplication.init()} on the context before it refreshes; it never becomes
|
||||
* a bean itself.
|
||||
*/
|
||||
public final class StartupProgressReporter implements BeanPostProcessor {
|
||||
|
||||
static final double CONTEXT_START = 0.10;
|
||||
static final double CONTEXT_END = 0.90;
|
||||
/** Below this change, a progress notification is not worth a round trip to the FX thread. */
|
||||
private static final double STEP = 0.01;
|
||||
|
||||
private enum EPhase {STARTING, OPENING_LIBRARY, LOADING, INTERFACE}
|
||||
|
||||
private final Consumer<SplashNotification> sink;
|
||||
|
||||
private int expected = 1;
|
||||
private int initialised;
|
||||
private double lastSent = -1;
|
||||
private EPhase phase = EPhase.STARTING;
|
||||
private @Nullable I18nService i18n;
|
||||
private @Nullable AppPreferences preferences;
|
||||
|
||||
public StartupProgressReporter(Consumer<SplashNotification> sink) {
|
||||
this.sink = sink;
|
||||
}
|
||||
|
||||
/** How many bean definitions the context holds once scanned — the denominator of the progress. */
|
||||
public void expect(int beanDefinitionCount) {
|
||||
expected = Math.max(1, beanDefinitionCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) {
|
||||
if (phase == EPhase.STARTING && bean instanceof LibraryService) {
|
||||
enter(EPhase.OPENING_LIBRARY);
|
||||
} else if (phase != EPhase.INTERFACE && bean.getClass().getPackageName().startsWith("org.icroco.pholio.ui")) {
|
||||
enter(EPhase.INTERFACE);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) {
|
||||
if (bean instanceof I18nService service) {
|
||||
i18n = service;
|
||||
} else if (bean instanceof AppPreferences prefs) {
|
||||
preferences = prefs;
|
||||
} else if (bean instanceof LibraryService && phase == EPhase.OPENING_LIBRARY) {
|
||||
enter(EPhase.LOADING);
|
||||
}
|
||||
initialised++;
|
||||
double progress = CONTEXT_START + (CONTEXT_END - CONTEXT_START) * Math.min(1.0, (double) initialised / expected);
|
||||
if (progress - lastSent >= STEP) {
|
||||
send(progress, null);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
/** {@code PholioFxApplication.start()}: the context is ready, the main window is being built. */
|
||||
public void interfaceStarted() {
|
||||
phase = EPhase.INTERFACE;
|
||||
send(CONTEXT_END, text("splash.preparingInterface"));
|
||||
}
|
||||
|
||||
private void enter(EPhase next) {
|
||||
phase = next;
|
||||
String message = switch (next) {
|
||||
case OPENING_LIBRARY -> libraryMessage();
|
||||
case LOADING -> text("splash.loadingComponents");
|
||||
case INTERFACE -> text("splash.preparingInterface");
|
||||
case STARTING -> null;
|
||||
};
|
||||
send(-1, message);
|
||||
}
|
||||
|
||||
private @Nullable String libraryMessage() {
|
||||
String library = preferences == null ? null : preferences.getValue("library", "last-opened", String.class);
|
||||
return library == null || library.isBlank()
|
||||
? text("splash.openingLibrary")
|
||||
: text("splash.openingLibraryNamed", library);
|
||||
}
|
||||
|
||||
private @Nullable String text(String key, Object... args) {
|
||||
return i18n == null ? null : i18n.get(key, args);
|
||||
}
|
||||
|
||||
private void send(double progress, @Nullable String message) {
|
||||
if (progress >= 0) {
|
||||
lastSent = progress;
|
||||
}
|
||||
sink.accept(SplashNotification.status(progress, message));
|
||||
}
|
||||
}
|
||||
@@ -307,3 +307,10 @@ task.startup.pruning.anonymous.persons=Cleaning up unnamed people
|
||||
task.startup.library.sync=Synchronizing library
|
||||
task.startup.geocoding.reference.data=Installing reverse geocoding data
|
||||
task.startup.checking.media.files=Checking media files
|
||||
|
||||
# SplashPreloader / StartupProgressReporter
|
||||
splash.starting=Starting…
|
||||
splash.openingLibrary=Opening your library…
|
||||
splash.openingLibraryNamed=Opening the library “{0}”…
|
||||
splash.loadingComponents=Loading components…
|
||||
splash.preparingInterface=Preparing the interface…
|
||||
|
||||
@@ -310,3 +310,10 @@ task.startup.pruning.anonymous.persons=Nettoyage des personnes anonymes
|
||||
task.startup.library.sync=Synchronisation de la bibliothèque
|
||||
task.startup.geocoding.reference.data=Installation des données de géocodage inverse
|
||||
task.startup.checking.media.files=Vérification des fichiers
|
||||
|
||||
# SplashPreloader / StartupProgressReporter
|
||||
splash.starting=Démarrage…
|
||||
splash.openingLibrary=Ouverture de votre photothèque…
|
||||
splash.openingLibraryNamed=Ouverture de la photothèque « {0} »…
|
||||
splash.loadingComponents=Chargement des composants…
|
||||
splash.preparingInterface=Préparation de l’interface…
|
||||
|
||||
@@ -27,6 +27,7 @@ class GuiBootstrapTest {
|
||||
|
||||
private static final String ENABLE_PREVIEW_PROPERTY = "javafx.enablePreview";
|
||||
private static final String DISABLE_PRISM_DIRTY_OPTS_PROPERTY = "prism.dirtyopts";
|
||||
private static final String PRELOADER_PROPERTY = "javafx.preloader";
|
||||
|
||||
// Saved, not just cleared: the pom sets javafx.enablePreview=true as a real JVM system property for the
|
||||
// whole Surefire fork (other test classes, e.g. AppHeaderBarTest, need it for real HeaderBar
|
||||
@@ -34,11 +35,14 @@ class GuiBootstrapTest {
|
||||
// this class's own tests run would silently break every later test class in the same fork.
|
||||
private @Nullable String originalEnablePreview;
|
||||
private @Nullable String originalDirtyOpts;
|
||||
private @Nullable String originalPreloader;
|
||||
|
||||
@BeforeEach
|
||||
void captureAndClearProperties() {
|
||||
originalEnablePreview = System.getProperty(ENABLE_PREVIEW_PROPERTY);
|
||||
originalDirtyOpts = System.getProperty(DISABLE_PRISM_DIRTY_OPTS_PROPERTY);
|
||||
originalPreloader = System.getProperty(PRELOADER_PROPERTY);
|
||||
System.clearProperty(PRELOADER_PROPERTY);
|
||||
System.clearProperty(ENABLE_PREVIEW_PROPERTY);
|
||||
System.clearProperty(DISABLE_PRISM_DIRTY_OPTS_PROPERTY);
|
||||
}
|
||||
@@ -47,6 +51,7 @@ class GuiBootstrapTest {
|
||||
void restoreProperties() {
|
||||
restore(ENABLE_PREVIEW_PROPERTY, originalEnablePreview);
|
||||
restore(DISABLE_PRISM_DIRTY_OPTS_PROPERTY, originalDirtyOpts);
|
||||
restore(PRELOADER_PROPERTY, originalPreloader);
|
||||
}
|
||||
|
||||
private static void restore(String property, @Nullable String value) {
|
||||
@@ -91,6 +96,17 @@ class GuiBootstrapTest {
|
||||
assertThat(System.getProperty(DISABLE_PRISM_DIRTY_OPTS_PROPERTY)).isEqualTo("true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void registersTheSplashScreenAsTheJavaFxPreloaderUnlessOneIsGiven(SoftAssertions softly) {
|
||||
GuiBootstrap.configureSystemProperties();
|
||||
softly.assertThat(System.getProperty(PRELOADER_PROPERTY))
|
||||
.isEqualTo("org.icroco.pholio.ui.splash.SplashPreloader");
|
||||
|
||||
System.setProperty(PRELOADER_PROPERTY, "");
|
||||
GuiBootstrap.configureSystemProperties();
|
||||
softly.assertThat(System.getProperty(PRELOADER_PROPERTY)).as("an explicit empty value disables it").isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotOverrideAnExplicitEnablePreviewChoice() {
|
||||
System.setProperty(ENABLE_PREVIEW_PROPERTY, "false");
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.icroco.pholio.ui.splash;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.icroco.pholio.infra.i18n.I18nService;
|
||||
import org.icroco.pholio.infra.library.LibraryService;
|
||||
import org.icroco.pholio.infra.preferences.AppPreferences;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class StartupProgressReporterTest {
|
||||
|
||||
@Test
|
||||
void announcesEachPhaseOnceInOrderAndProgressesWithTheBeans() {
|
||||
List<SplashNotification> sent = new ArrayList<>();
|
||||
StartupProgressReporter reporter = new StartupProgressReporter(sent::add);
|
||||
reporter.expect(10);
|
||||
|
||||
I18nService i18n = mock(I18nService.class);
|
||||
when(i18n.get(anyString(), any(Object[].class))).thenAnswer(call -> {
|
||||
Object[] args = call.getArguments();
|
||||
return args.length > 1 && args[1] != null ? args[0] + ":" + args[1] : String.valueOf(args[0]);
|
||||
});
|
||||
AppPreferences preferences = mock(AppPreferences.class);
|
||||
when(preferences.getValue("library", "last-opened", String.class)).thenReturn("Photos");
|
||||
|
||||
initialise(reporter, i18n, "i18n");
|
||||
initialise(reporter, preferences, "preferences");
|
||||
initialise(reporter, new Object(), "someInfraBean"); // still starting: no message
|
||||
initialise(reporter, mock(LibraryService.class), "libraryService"); // opening the library, then loading
|
||||
initialise(reporter, SplashNotification.finished(), "uiBean"); // a ui-package bean: interface
|
||||
reporter.interfaceStarted();
|
||||
|
||||
List<String> messages = sent.stream().map(SplashNotification::message).filter(Objects::nonNull).toList();
|
||||
double lastProgress = sent.stream().mapToDouble(SplashNotification::progress).filter(p -> p >= 0).max().orElse(0);
|
||||
|
||||
SoftAssertions softly = new SoftAssertions();
|
||||
softly.assertThat(messages).containsExactly(
|
||||
"splash.openingLibraryNamed:Photos",
|
||||
"splash.loadingComponents",
|
||||
"splash.preparingInterface",
|
||||
"splash.preparingInterface");
|
||||
softly.assertThat(sent.stream().mapToDouble(SplashNotification::progress).filter(p -> p >= 0))
|
||||
.as("never goes backwards").isSorted();
|
||||
softly.assertThat(lastProgress).isEqualTo(StartupProgressReporter.CONTEXT_END);
|
||||
softly.assertThat(sent).noneMatch(SplashNotification::done);
|
||||
softly.assertAll();
|
||||
}
|
||||
|
||||
private static void initialise(StartupProgressReporter reporter, Object bean, String name) {
|
||||
reporter.postProcessBeforeInitialization(bean, name);
|
||||
reporter.postProcessAfterInitialization(bean, name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user