diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java
index 411a57a..e89b049 100644
--- a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java
@@ -89,6 +89,7 @@ public class AccountViewController implements RouteSelectionListener {
@FXML
public void archiveAccount() {
boolean confirmResult = Popups.confirm(
+ titleLabel,
"Are you sure you want to archive this account? It will no " +
"longer show up in the app normally, and you won't be " +
"able to add new transactions to it. You'll still be " +
@@ -103,6 +104,7 @@ public class AccountViewController implements RouteSelectionListener {
@FXML public void unarchiveAccount() {
boolean confirm = Popups.confirm(
+ titleLabel,
"Are you sure you want to restore this account from its archived " +
"status?"
);
@@ -115,6 +117,7 @@ public class AccountViewController implements RouteSelectionListener {
@FXML
public void deleteAccount() {
boolean confirm = Popups.confirm(
+ titleLabel,
"Are you sure you want to permanently delete this account and " +
"all data directly associated with it? This cannot be " +
"undone; deleted accounts are not recoverable at all. " +
diff --git a/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java b/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java
index 32334ca..9e2c4d4 100644
--- a/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/BalanceRecordViewController.java
@@ -48,7 +48,10 @@ public class BalanceRecordViewController implements RouteSelectionListener {
}
@FXML public void delete() {
- boolean confirm = Popups.confirm("Are you sure you want to delete this balance record? This may have an effect on the derived balance of your account, as shown in Perfin.");
+ boolean confirm = Popups.confirm(
+ titleLabel,
+ "Are you sure you want to delete this balance record? This may have an effect on the derived balance of your account, as shown in Perfin."
+ );
if (confirm) {
Profile.getCurrent().dataSource().useRepo(BalanceRecordRepository.class, repo -> repo.deleteById(balanceRecord.id));
router.navigateBackAndClear();
diff --git a/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java b/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
index 68d1657..a9233ff 100644
--- a/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/CreateBalanceRecordController.java
@@ -89,7 +89,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
LocalDateTime localTimestamp = LocalDateTime.parse(timestampField.getText(), DateUtil.DEFAULT_DATETIME_FORMAT);
BigDecimal reportedBalance = new BigDecimal(balanceField.getText());
- boolean confirm = Popups.confirm("Are you sure that you want to record the balance of account\n%s\nas %s,\nas of %s?".formatted(
+ boolean confirm = Popups.confirm(timestampField, "Are you sure that you want to record the balance of account\n%s\nas %s,\nas of %s?".formatted(
account.getShortName(),
CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(reportedBalance, account.getCurrency())),
localTimestamp.atZone(ZoneId.systemDefault()).format(DateUtil.DEFAULT_DATETIME_FORMAT_WITH_ZONE)
@@ -122,7 +122,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
CurrencyUtil.formatMoney(new MoneyValue(reportedBalance, account.getCurrency())),
CurrencyUtil.formatMoney(new MoneyValue(currentDerivedBalance, account.getCurrency()))
);
- return Popups.confirm(msg);
+ return Popups.confirm(timestampField, msg);
}
return true;
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java b/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java
index 3e3142e..2d97e97 100644
--- a/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/EditAccountController.java
@@ -117,7 +117,7 @@ public class EditAccountController implements RouteSelectionListener {
BigDecimal initialBalance = new BigDecimal(initialBalanceField.getText().strip());
List attachments = Collections.emptyList();
- boolean success = Popups.confirm("Are you sure you want to create this account?");
+ boolean success = Popups.confirm(accountNameField, "Are you sure you want to create this account?");
if (success) {
long id = accountRepo.insert(type, number, name, currency);
balanceRepo.insert(LocalDateTime.now(ZoneOffset.UTC), id, initialBalance, currency, attachments);
@@ -138,7 +138,7 @@ public class EditAccountController implements RouteSelectionListener {
}
} catch (Exception e) {
log.error("Failed to save (or update) account " + account.id, e);
- Popups.error("Failed to save the account: " + e.getMessage());
+ Popups.error(accountNameField, "Failed to save the account: " + e.getMessage());
}
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java b/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java
index 6bebcd8..5db7345 100644
--- a/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java
@@ -203,7 +203,7 @@ public class EditTransactionController implements RouteSelectionListener {
});
} catch (Exception e) {
log.error("Failed to get repositories.", e);
- Popups.error("Failed to fetch account-specific data: " + e.getMessage());
+ Popups.error(container, "Failed to fetch account-specific data: " + e.getMessage());
}
});
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/Popups.java b/src/main/java/com/andrewlalis/perfin/control/Popups.java
index eb2c5d4..ff2af1f 100644
--- a/src/main/java/com/andrewlalis/perfin/control/Popups.java
+++ b/src/main/java/com/andrewlalis/perfin/control/Popups.java
@@ -1,30 +1,65 @@
package com.andrewlalis.perfin.control;
+import javafx.scene.Node;
+import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.stage.Modality;
+import javafx.stage.Window;
/**
* Helper class for standardized popups and confirmation dialogs for the app.
*/
public class Popups {
- public static boolean confirm(String text) {
+ public static boolean confirm(Window owner, String text) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, text);
+ alert.initOwner(owner);
alert.initModality(Modality.APPLICATION_MODAL);
var result = alert.showAndWait();
return result.isPresent() && result.get() == ButtonType.OK;
}
- public static void message(String text) {
+ public static boolean confirm(Node node, String text) {
+ return confirm(getWindowFromNode(node), text);
+ }
+
+ public static void message(Window owner, String text) {
Alert alert = new Alert(Alert.AlertType.NONE, text);
+ alert.initOwner(owner);
alert.initModality(Modality.APPLICATION_MODAL);
alert.getButtonTypes().setAll(ButtonType.OK);
alert.showAndWait();
}
- public static void error(String text) {
+ public static void message(Node node, String text) {
+ message(getWindowFromNode(node), text);
+ }
+
+ public static void error(Window owner, String text) {
Alert alert = new Alert(Alert.AlertType.WARNING, text);
+ alert.initOwner(owner);
alert.initModality(Modality.APPLICATION_MODAL);
alert.showAndWait();
}
+
+ public static void error(Node node, String text) {
+ error(getWindowFromNode(node), text);
+ }
+
+ public static void error(Window owner, Exception e) {
+ error(owner, "An " + e.getClass().getSimpleName() + " occurred: " + e.getMessage());
+ }
+
+ public static void error(Node node, Exception e) {
+ error(getWindowFromNode(node), e);
+ }
+
+ private static Window getWindowFromNode(Node n) {
+ Window owner = null;
+ Scene scene = n.getScene();
+ if (scene != null) {
+ owner = scene.getWindow();
+ }
+ return owner;
+ }
}
diff --git a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java
index d9bb663..c26665a 100644
--- a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java
@@ -46,10 +46,10 @@ public class ProfilesViewController {
String name = newProfileNameField.getText();
boolean valid = Profile.validateName(name);
if (valid && !ProfileLoader.getAvailableProfiles().contains(name)) {
- boolean confirm = Popups.confirm("Are you sure you want to add a new profile named \"" + name + "\"?");
+ boolean confirm = Popups.confirm(profilesVBox, "Are you sure you want to add a new profile named \"" + name + "\"?");
if (confirm) {
if (openProfile(name, false)) {
- Popups.message("Created new profile \"" + name + "\" and loaded it.");
+ Popups.message(profilesVBox, "Created new profile \"" + name + "\" and loaded it.");
}
newProfileNameField.clear();
}
@@ -108,18 +108,18 @@ public class ProfilesViewController {
PerfinApp.profileLoader.load(name);
ProfilesStage.closeView();
router.replace("accounts");
- if (showPopup) Popups.message("The profile \"" + name + "\" has been loaded.");
+ if (showPopup) Popups.message(profilesVBox, "The profile \"" + name + "\" has been loaded.");
return true;
} catch (ProfileLoadException e) {
- Popups.error("Failed to load the profile: " + e.getMessage());
+ Popups.error(profilesVBox, "Failed to load the profile: " + e.getMessage());
return false;
}
}
private void deleteProfile(String name) {
- boolean confirmA = Popups.confirm("Are you sure you want to delete the profile \"" + name + "\"? This will permanently delete ALL accounts, transactions, files, and other data for this profile, and it cannot be recovered.");
+ boolean confirmA = Popups.confirm(profilesVBox, "Are you sure you want to delete the profile \"" + name + "\"? This will permanently delete ALL accounts, transactions, files, and other data for this profile, and it cannot be recovered.");
if (confirmA) {
- boolean confirmB = Popups.confirm("Press \"OK\" to confirm that you really want to delete the profile \"" + name + "\". There's no going back.");
+ boolean confirmB = Popups.confirm(profilesVBox, "Press \"OK\" to confirm that you really want to delete the profile \"" + name + "\". There's no going back.");
if (confirmB) {
try {
FileUtil.deleteDirRecursive(Profile.getDir(name));
diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
index 7315ec0..8884c8d 100644
--- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java
@@ -72,6 +72,7 @@ public class TransactionViewController {
@FXML public void deleteTransaction() {
boolean confirm = Popups.confirm(
+ titleLabel,
"Are you sure you want to delete this transaction? This will " +
"permanently remove the transaction and its effects on any linked " +
"accounts, as well as remove any attachments from storage within " +
diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java
index 6db70a3..d9a6d73 100644
--- a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java
+++ b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java
@@ -177,7 +177,7 @@ public class TransactionsViewController implements RouteSelectionListener {
));
}
} catch (Exception e) {
- Popups.error("An error occurred: " + e.getMessage());
+ Popups.error(transactionsListBorderPane, e);
}
}
}
diff --git a/src/main/java/com/andrewlalis/perfin/model/Profile.java b/src/main/java/com/andrewlalis/perfin/model/Profile.java
index 992dddc..de4647b 100644
--- a/src/main/java/com/andrewlalis/perfin/model/Profile.java
+++ b/src/main/java/com/andrewlalis/perfin/model/Profile.java
@@ -28,6 +28,10 @@ import java.util.function.Consumer;
* class maintains a static current profile that can be loaded and
* unloaded.
*
+ *
+ * @param name The name of the profile.
+ * @param settings The profile's settings.
+ * @param dataSource The profile's data source.
*/
public record Profile(String name, Properties settings, DataSource dataSource) {
private static final Logger log = LoggerFactory.getLogger(Profile.class);
@@ -65,6 +69,7 @@ public record Profile(String name, Properties settings, DataSource dataSource) {
}
}
currentProfileListeners.removeIf(ref -> ref.get() == null);
+ log.debug("Current profile set to {}.", current.name());
}
public static void whenLoaded(Consumer consumer) {
diff --git a/src/main/java/com/andrewlalis/perfin/model/ProfileLoader.java b/src/main/java/com/andrewlalis/perfin/model/ProfileLoader.java
index 046ea58..4b58b2d 100644
--- a/src/main/java/com/andrewlalis/perfin/model/ProfileLoader.java
+++ b/src/main/java/com/andrewlalis/perfin/model/ProfileLoader.java
@@ -1,6 +1,7 @@
package com.andrewlalis.perfin.model;
import com.andrewlalis.perfin.PerfinApp;
+import com.andrewlalis.perfin.control.Popups;
import com.andrewlalis.perfin.data.DataSourceFactory;
import com.andrewlalis.perfin.data.ProfileLoadException;
import com.andrewlalis.perfin.data.util.FileUtil;
@@ -43,6 +44,21 @@ public class ProfileLoader {
} catch (IOException e) {
throw new ProfileLoadException("Failed to load profile settings.", e);
}
+ try {
+ DataSourceFactory.SchemaStatus status = dataSourceFactory.getSchemaStatus(name);
+ if (status == DataSourceFactory.SchemaStatus.NEEDS_MIGRATION) {
+ boolean confirm = Popups.confirm(window, "The profile \"" + name + "\" has an outdated data schema and needs to be migrated to the latest version. Is this okay?");
+ if (!confirm) {
+ throw new ProfileLoadException("User rejected migration.");
+ }
+ } else if (status == DataSourceFactory.SchemaStatus.INCOMPATIBLE) {
+ Popups.error(window, "The profile \"" + name + "\" has a data schema that's incompatible with this app.");
+ throw new ProfileLoadException("Incompatible schema version.");
+ }
+ } catch (IOException e) {
+ throw new ProfileLoadException("Failed to get profile's schema status.", e);
+ }
+ Popups.message(window, "Test!");
return new Profile(name, settings, dataSourceFactory.getDataSource(name));
}
diff --git a/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java b/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java
index 526951d..c0d9994 100644
--- a/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java
+++ b/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java
@@ -9,6 +9,7 @@ import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
@@ -60,6 +61,10 @@ public class StartupSplashScreen extends Stage implements Consumer {
return scene;
}
+ /**
+ * Runs all tasks sequentially, invoking each one on the JavaFX main thread,
+ * and quitting if there's any exception thrown.
+ */
private void runTasks() {
Thread.ofVirtual().start(() -> {
try {
@@ -69,7 +74,16 @@ public class StartupSplashScreen extends Stage implements Consumer {
}
for (var task : tasks) {
try {
- task.accept(this);
+ CompletableFuture future = new CompletableFuture<>();
+ Platform.runLater(() -> {
+ try {
+ task.accept(this);
+ future.complete(null);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ });
+ future.join();
Thread.sleep(500);
} catch (Exception e) {
accept("Startup failed: " + e.getMessage());