StartupSplash.java
/*
* Copyright 2026, Aiki IT, FotoRenamer
*
* 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 de.aikiit.fotorenamer.gui;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;
import java.awt.GraphicsEnvironment;
import java.awt.SplashScreen;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
/**
* Uses the JVM splash when available, otherwise shows the bundled splash photograph
* in a Swing window. Show and close on the Swing event dispatch thread.
*/
public final class StartupSplash implements AutoCloseable {
private static final Logger LOG = LogManager.getLogger(StartupSplash.class);
static final String IMAGE_LOCATION = "/image/splash-camera.jpg";
private final SplashScreen nativeSplash;
private final JWindow window;
private boolean closed;
StartupSplash(final SplashScreen nativeSplash, final JWindow window) {
this.nativeSplash = nativeSplash;
this.window = window;
}
/**
* Shows the splash without delaying application startup artificially.
* In headless mode, no native or Swing splash is accessed.
*
* @return A splash handle to close when the main window is visible or startup fails.
*/
public static StartupSplash show() {
if (GraphicsEnvironment.isHeadless()) {
return new StartupSplash(null, null);
}
if (!SwingUtilities.isEventDispatchThread()) {
throw new IllegalStateException("Show the splash on the Swing event dispatch thread");
}
JWindow fallback = null;
try {
SplashScreen nativeSplash = SplashScreen.getSplashScreen();
if (nativeSplash != null) {
return new StartupSplash(nativeSplash, null);
}
URL resource = StartupSplash.class.getResource(IMAGE_LOCATION);
if (resource == null) {
throw new IOException("Splash image not found: " + IMAGE_LOCATION);
}
BufferedImage image = ImageIO.read(resource);
if (image == null) {
throw new IOException("Unable to decode splash image: " + IMAGE_LOCATION);
}
fallback = new JWindow();
fallback.setFocusableWindowState(false);
fallback.getContentPane().add(new JLabel(new ImageIcon(image)));
fallback.pack();
fallback.setLocationRelativeTo(null);
fallback.setVisible(true);
// Paint before main-window initialization occupies the event thread.
fallback.getRootPane().paintImmediately(0, 0, fallback.getWidth(), fallback.getHeight());
return new StartupSplash(null, fallback);
} catch (IOException | RuntimeException e) {
LOG.warn("Unable to show startup splash using '{}'", IMAGE_LOCATION, e);
StartupSplash failedSplash = new StartupSplash(null, fallback);
failedSplash.close();
return failedSplash;
}
}
/** Closes the splash, including on startup failure. Safe to call repeatedly. */
@Override
public void close() {
if (closed) {
return;
}
closed = true;
try {
// The JVM normally closes its splash when the main window appears.
if (nativeSplash != null && nativeSplash.isVisible()) {
nativeSplash.close();
}
if (window != null) {
window.dispose();
}
} catch (RuntimeException e) {
LOG.warn("Unable to close startup splash", e);
}
}
}