Improved splash screen ergonomics.
This commit is contained in:
parent
b81b27770e
commit
f6dbc50155
|
@ -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.).
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
Loading…
Reference in New Issue