Improved splash screen ergonomics.

This commit is contained in:
Andrew Lalis 2023-12-14 18:29:03 -05:00
parent b81b27770e
commit f6dbc50155
6 changed files with 155 additions and 61 deletions

View File

@ -2,4 +2,11 @@
> *Per*sonal *Fin*ance > *Per*sonal *Fin*ance
Personal accounting desktop app to track your finances using common file formats. A personal accounting desktop app to track your finances using an approachable
interface and interoperable file formats for maximum compatibility.
## About Perfin
Perfin is a desktop app built with Java 21 and JavaFX, using the SQLite3
database for most data storage. It's intended to be used by individuals to
track their finances across multiple accounts (savings, checking, credit, etc.).

View File

@ -1,23 +1,28 @@
package com.andrewlalis.perfin; package com.andrewlalis.perfin;
import com.andrewlalis.perfin.view.SplashScreenStage;
import javafx.application.Application; import javafx.application.Application;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.nio.file.Path;
/**
* The class from which the JavaFX-based application starts.
*/
public class PerfinApp extends Application { public class PerfinApp extends Application {
public static final Path APP_DIR = Path.of(System.getProperty("user.home", "."), ".perfin");
public static void main(String[] args) { public static void main(String[] args) {
launch(args); launch(args);
} }
@Override @Override
public void start(Stage stage) { public void start(Stage stage) {
SplashScreenStage splashStage = new SplashScreenStage("Loading", SceneUtil.load("/startup-splash-screen.fxml"));
splashStage.show();
initMainScreen(stage); initMainScreen(stage);
Stage splashStage = showStartupSplashScreen(); splashStage.stateProperty().addListener((v, oldState, state) -> {
// Once the splash stage is hidden, show the main stage. if (state == SplashScreenStage.State.DONE) stage.show();
splashStage.showingProperty().not().addListener((v, old, hidden) -> {
if (hidden) {
showMainScreen(stage);
}
}); });
} }
@ -26,24 +31,4 @@ public class PerfinApp extends Application {
stage.setScene(SceneUtil.load("/main.fxml")); stage.setScene(SceneUtil.load("/main.fxml"));
stage.setTitle("Perfin"); stage.setTitle("Perfin");
} }
private void showMainScreen(Stage stage) {
System.out.println("Showing the main application.");
// stage.setMaximized(true);
stage.show();
}
/**
* Shows a startup "splash" screen for a short time.
* @return The stage in which the splash screen is shown.
*/
private Stage showStartupSplashScreen() {
Stage stage = new Stage();
stage.initStyle(StageStyle.UNDECORATED);
stage.setScene(SceneUtil.load("/startup-splash-screen.fxml"));
stage.setTitle("Loading");
stage.setResizable(false);
stage.show();
return stage;
}
} }

View File

@ -1,36 +1,81 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.perfin.view.SplashScreenStage;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Label; import javafx.scene.control.TextArea;
import javafx.stage.Stage; import javafx.scene.layout.BorderPane;
import java.io.IOException;
import java.nio.file.Files;
import static com.andrewlalis.perfin.PerfinApp.APP_DIR;
/**
* A controller for the application's splash screen that shows initially on
* startup. While the splash screen is shown, we do any complicated loading
* tasks so that the application starts properly, and give the user periodic
* updates as we go.
*/
public class StartupSplashScreenController { public class StartupSplashScreenController {
@FXML @FXML
public Label content; public BorderPane sceneRoot;
@FXML
public TextArea content;
@FXML @FXML
public void initialize() { public void initialize() {
content.setText("Loading Perfin...");
Thread.ofVirtual().start(() -> { Thread.ofVirtual().start(() -> {
try { try {
Thread.sleep(1000); printlnLater("Initializing application files...");
if (!initAppDir()) {
Platform.runLater(() -> getSplashStage().setError());
return;
}
Platform.runLater(() -> content.setText("Still loading...")); printlnLater("Perfin initialized. Starting the app now.");
Thread.sleep(1000); Thread.sleep(2000);
Platform.runLater(() -> content.setText("Almost done...")); Platform.runLater(() -> getSplashStage().setDone());
Thread.sleep(1000); } catch (Exception e) {
e.printStackTrace(System.err);
Platform.runLater(() -> content.setText("Done!")); printlnLater("An error occurred while starting: " + e.getMessage() + "\nThe application will now exit.");
Thread.sleep(500); Platform.runLater(() -> getSplashStage().setError());
System.out.println("Closing splash screen...");
Stage stage = (Stage) content.getScene().getWindow();
Platform.runLater(stage::close);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} }
}); });
} }
private void println(String text) {
content.appendText(text + "\n");
}
private void printlnLater(String text) {
Platform.runLater(() -> println(text));
}
private SplashScreenStage getSplashStage() {
return (SplashScreenStage) sceneRoot.getScene().getWindow();
}
private boolean initAppDir() {
if (Files.notExists(APP_DIR)) {
printlnLater(APP_DIR + " doesn't exist yet. Creating it now.");
try {
Files.createDirectory(APP_DIR);
} catch (IOException e) {
printlnLater("Could not create directory " + APP_DIR + "; " + e.getMessage());
return false;
}
} else if (Files.exists(APP_DIR) && Files.isRegularFile(APP_DIR)) {
printlnLater(APP_DIR + " is a file, when it should be a directory. Deleting it and creating new directory.");
try {
Files.delete(APP_DIR);
Files.createDirectory(APP_DIR);
} catch (IOException e) {
printlnLater("Could not delete file and create directory " + APP_DIR + "; " + e.getMessage());
return false;
}
}
return true;
}
} }

View File

@ -0,0 +1,38 @@
package com.andrewlalis.perfin.view;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class SplashScreenStage extends Stage {
public enum State {
LOADING,
DONE,
ERROR
}
private final SimpleObjectProperty<State> stateProperty = new SimpleObjectProperty<>(State.LOADING);
public SplashScreenStage(String title, Scene scene) {
setTitle(title);
setResizable(false);
initStyle(StageStyle.UNDECORATED);
setScene(scene);
}
public void setDone() {
stateProperty.set(State.DONE);
close();
}
public void setError() {
stateProperty.set(State.ERROR);
close();
}
public ObservableValue<State> stateProperty() {
return this.stateProperty;
}
}

View File

@ -1,11 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.AnchorPane?> <?import javafx.scene.layout.*?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
<BorderPane xmlns="http://javafx.com/javafx/17.0.2-ea"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.andrewlalis.perfin.control.MainController" fx:controller="com.andrewlalis.perfin.control.MainController"
minWidth="600.0" minHeight="400.0" minWidth="600.0" minHeight="400.0">
> <top>
<Label>Main UI</Label> <MenuBar BorderPane.alignment="CENTER">
</AnchorPane> <Menu mnemonicParsing="false" text="File">
<MenuItem mnemonicParsing="false" text="Close"/>
</Menu>
<Menu mnemonicParsing="false" text="Edit">
<MenuItem mnemonicParsing="false" text="Delete"/>
</Menu>
<Menu mnemonicParsing="false" text="Help">
<MenuItem mnemonicParsing="false" text="About"/>
</Menu>
</MenuBar>
</top>
</BorderPane>

View File

@ -1,11 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?> <?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.BorderPane?>
<BorderPane fx:id="sceneRoot" prefHeight="200" prefWidth="400" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.andrewlalis.perfin.control.StartupSplashScreenController">
<padding><Insets top="10" bottom="10" left="10" right="10"/></padding>
<top>
<Label BorderPane.alignment="CENTER_LEFT" style="-fx-font-weight: bold; -fx-font-size: large;">Perfin is starting up...</Label>
</top>
<AnchorPane xmlns="http://javafx.com/javafx" <center>
xmlns:fx="http://javafx.com/fxml" <TextArea fx:id="content" wrapText="true" editable="false" style="-fx-font-family: monospace" focusTraversable="false"/>
fx:controller="com.andrewlalis.perfin.control.StartupSplashScreenController" </center>
prefHeight="200" prefWidth="400">
<Label fx:id="content" wrapText="true"/> </BorderPane>
</AnchorPane>