Added profiles selector and logic for changing the selected profile.
This commit is contained in:
parent
7d7f80676a
commit
755dc87aec
|
@ -35,7 +35,7 @@ public class PerfinApp extends Application {
|
||||||
PerfinApp::defineRoutes,
|
PerfinApp::defineRoutes,
|
||||||
PerfinApp::initAppDir,
|
PerfinApp::initAppDir,
|
||||||
c -> initMainScreen(stage, c),
|
c -> initMainScreen(stage, c),
|
||||||
PerfinApp::loadProfile
|
PerfinApp::loadLastUsedProfile
|
||||||
));
|
));
|
||||||
splashScreen.showAndWait();
|
splashScreen.showAndWait();
|
||||||
if (splashScreen.isStartupSuccessful()) {
|
if (splashScreen.isStartupSuccessful()) {
|
||||||
|
@ -43,7 +43,7 @@ public class PerfinApp extends Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initMainScreen(Stage stage, Consumer<String> msgConsumer) throws Exception {
|
private void initMainScreen(Stage stage, Consumer<String> msgConsumer) {
|
||||||
msgConsumer.accept("Initializing main screen.");
|
msgConsumer.accept("Initializing main screen.");
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
stage.hide();
|
stage.hide();
|
||||||
|
@ -57,7 +57,7 @@ public class PerfinApp extends Application {
|
||||||
router.map(route, PerfinApp.class.getResource(resource));
|
router.map(route, PerfinApp.class.getResource(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void defineRoutes(Consumer<String> msgConsumer) throws Exception {
|
private static void defineRoutes(Consumer<String> msgConsumer) {
|
||||||
msgConsumer.accept("Initializing application views.");
|
msgConsumer.accept("Initializing application views.");
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
mapResourceRoute("accounts", "/accounts-view.fxml");
|
mapResourceRoute("accounts", "/accounts-view.fxml");
|
||||||
|
@ -80,7 +80,7 @@ public class PerfinApp extends Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void loadProfile(Consumer<String> msgConsumer) throws Exception {
|
private static void loadLastUsedProfile(Consumer<String> msgConsumer) throws Exception {
|
||||||
msgConsumer.accept("Loading the most recent profile.");
|
msgConsumer.accept("Loading the most recent profile.");
|
||||||
Profile.loadLast();
|
Profile.loadLast();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.andrewlalis.perfin.control;
|
||||||
|
|
||||||
import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView;
|
import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView;
|
||||||
import com.andrewlalis.perfin.view.BindingUtil;
|
import com.andrewlalis.perfin.view.BindingUtil;
|
||||||
|
import com.andrewlalis.perfin.view.ProfilesStage;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
|
@ -10,13 +11,10 @@ import javafx.scene.layout.HBox;
|
||||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
public class MainViewController {
|
public class MainViewController {
|
||||||
@FXML
|
@FXML public BorderPane mainContainer;
|
||||||
public BorderPane mainContainer;
|
@FXML public HBox breadcrumbHBox;
|
||||||
@FXML
|
|
||||||
public HBox breadcrumbHBox;
|
|
||||||
|
|
||||||
@FXML
|
@FXML public void initialize() {
|
||||||
public void initialize() {
|
|
||||||
AnchorPaneRouterView routerView = (AnchorPaneRouterView) router.getView();
|
AnchorPaneRouterView routerView = (AnchorPaneRouterView) router.getView();
|
||||||
mainContainer.setCenter(routerView.getAnchorPane());
|
mainContainer.setCenter(routerView.getAnchorPane());
|
||||||
|
|
||||||
|
@ -36,25 +34,25 @@ public class MainViewController {
|
||||||
router.navigate("accounts");
|
router.navigate("accounts");
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML public void goBack() {
|
||||||
public void goBack() {
|
|
||||||
router.navigateBack();
|
router.navigateBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML public void goForward() {
|
||||||
public void goForward() {
|
|
||||||
router.navigateForward();
|
router.navigateForward();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML public void goToAccounts() {
|
||||||
public void goToAccounts() {
|
|
||||||
router.getHistory().clear();
|
router.getHistory().clear();
|
||||||
router.navigate("accounts");
|
router.navigate("accounts");
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML public void goToTransactions() {
|
||||||
public void goToTransactions() {
|
|
||||||
router.getHistory().clear();
|
router.getHistory().clear();
|
||||||
router.navigate("transactions");
|
router.navigate("transactions");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML public void viewProfiles() {
|
||||||
|
ProfilesStage.open(mainContainer.getScene().getWindow());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,4 +11,17 @@ public class Popups {
|
||||||
var result = alert.showAndWait();
|
var result = alert.showAndWait();
|
||||||
return result.isPresent() && result.get() == ButtonType.OK;
|
return result.isPresent() && result.get() == ButtonType.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void message(String text) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.NONE, text);
|
||||||
|
alert.initModality(Modality.APPLICATION_MODAL);
|
||||||
|
alert.getButtonTypes().setAll(ButtonType.OK);
|
||||||
|
alert.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void error(String text) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.WARNING, text);
|
||||||
|
alert.initModality(Modality.APPLICATION_MODAL);
|
||||||
|
alert.showAndWait();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
package com.andrewlalis.perfin.control;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.data.FileUtil;
|
||||||
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
|
import com.andrewlalis.perfin.view.ProfilesStage;
|
||||||
|
import javafx.beans.binding.BooleanExpression;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.AnchorPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextFlow;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
|
public class ProfilesViewController {
|
||||||
|
@FXML public VBox profilesVBox;
|
||||||
|
@FXML public TextField newProfileNameField;
|
||||||
|
@FXML public Text newProfileNameErrorLabel;
|
||||||
|
@FXML public Button addProfileButton;
|
||||||
|
|
||||||
|
@FXML public void initialize() {
|
||||||
|
BooleanExpression newProfileNameValid = BooleanProperty.booleanExpression(newProfileNameField.textProperty()
|
||||||
|
.map(text -> (
|
||||||
|
text != null &&
|
||||||
|
!text.isBlank() &&
|
||||||
|
Profile.validateName(text) &&
|
||||||
|
!Profile.getAvailableProfiles().contains(text)
|
||||||
|
)));
|
||||||
|
newProfileNameErrorLabel.managedProperty().bind(newProfileNameErrorLabel.visibleProperty());
|
||||||
|
newProfileNameErrorLabel.visibleProperty().bind(newProfileNameValid.not().and(newProfileNameField.textProperty().isNotEmpty()));
|
||||||
|
newProfileNameErrorLabel.wrappingWidthProperty().bind(newProfileNameField.widthProperty());
|
||||||
|
addProfileButton.disableProperty().bind(newProfileNameValid.not());
|
||||||
|
|
||||||
|
refreshAvailableProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML public void addProfile() {
|
||||||
|
String name = newProfileNameField.getText();
|
||||||
|
boolean valid = Profile.validateName(name);
|
||||||
|
if (valid && !Profile.getAvailableProfiles().contains(name)) {
|
||||||
|
boolean confirm = Popups.confirm("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.");
|
||||||
|
}
|
||||||
|
newProfileNameField.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshAvailableProfiles() {
|
||||||
|
List<String> profileNames = Profile.getAvailableProfiles();
|
||||||
|
String currentProfile = Profile.getCurrent() == null ? null : Profile.getCurrent().getName();
|
||||||
|
List<Node> nodes = new ArrayList<>(profileNames.size());
|
||||||
|
for (String profileName : profileNames) {
|
||||||
|
boolean isCurrent = profileName.equals(currentProfile);
|
||||||
|
AnchorPane profilePane = new AnchorPane();
|
||||||
|
profilePane.setStyle("""
|
||||||
|
-fx-border-color: lightgray;
|
||||||
|
-fx-border-radius: 5px;
|
||||||
|
-fx-padding: 5px;
|
||||||
|
""");
|
||||||
|
|
||||||
|
Text nameTextElement = new Text(profileName);
|
||||||
|
nameTextElement.setStyle("-fx-font-size: large;");
|
||||||
|
TextFlow nameLabel = new TextFlow(nameTextElement);
|
||||||
|
if (isCurrent) {
|
||||||
|
nameTextElement.setStyle("-fx-font-size: large; -fx-font-weight: bold;");
|
||||||
|
Text currentProfileIndicator = new Text(" Currently Selected Profile");
|
||||||
|
currentProfileIndicator.setStyle("""
|
||||||
|
-fx-font-size: small;
|
||||||
|
-fx-fill: grey;
|
||||||
|
""");
|
||||||
|
nameLabel.getChildren().add(currentProfileIndicator);
|
||||||
|
}
|
||||||
|
AnchorPane.setLeftAnchor(nameLabel, 0.0);
|
||||||
|
AnchorPane.setTopAnchor(nameLabel, 0.0);
|
||||||
|
AnchorPane.setBottomAnchor(nameLabel, 0.0);
|
||||||
|
|
||||||
|
HBox buttonBox = new HBox();
|
||||||
|
AnchorPane.setRightAnchor(buttonBox, 0.0);
|
||||||
|
AnchorPane.setTopAnchor(buttonBox, 0.0);
|
||||||
|
AnchorPane.setBottomAnchor(buttonBox, 0.0);
|
||||||
|
buttonBox.getStyleClass().addAll("std-spacing");
|
||||||
|
Button openButton = new Button("Open");
|
||||||
|
openButton.setOnAction(event -> openProfile(profileName, false));
|
||||||
|
openButton.setDisable(isCurrent);
|
||||||
|
buttonBox.getChildren().add(openButton);
|
||||||
|
Button deleteButton = new Button("Delete");
|
||||||
|
deleteButton.setOnAction(event -> deleteProfile(profileName));
|
||||||
|
buttonBox.getChildren().add(deleteButton);
|
||||||
|
|
||||||
|
profilePane.getChildren().setAll(nameLabel, buttonBox);
|
||||||
|
nodes.add(profilePane);
|
||||||
|
}
|
||||||
|
profilesVBox.getChildren().setAll(nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean openProfile(String name, boolean showPopup) {
|
||||||
|
System.out.println("Opening profile: " + name);
|
||||||
|
try {
|
||||||
|
Profile.load(name);
|
||||||
|
ProfilesStage.closeView();
|
||||||
|
router.getHistory().clear();
|
||||||
|
router.navigate("accounts");
|
||||||
|
if (showPopup) Popups.message("The profile \"" + name + "\" has been loaded.");
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace(System.err);
|
||||||
|
Popups.error("Failed to load 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.");
|
||||||
|
if (confirmA) {
|
||||||
|
boolean confirmB = Popups.confirm("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));
|
||||||
|
// Reset the app's "last profile" to the default if it was the deleted profile.
|
||||||
|
if (Profile.getLastProfile().equals(name)) {
|
||||||
|
Profile.saveLastProfile("default");
|
||||||
|
}
|
||||||
|
// If the current profile was deleted, switch to the default.
|
||||||
|
if (Profile.getCurrent() != null && Profile.getCurrent().getName().equals(name)) {
|
||||||
|
openProfile("default", true);
|
||||||
|
}
|
||||||
|
refreshAvailableProfiles();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,11 @@
|
||||||
package com.andrewlalis.perfin.data;
|
package com.andrewlalis.perfin.data;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -22,4 +28,20 @@ public class FileUtil {
|
||||||
MIMETYPES.put(".bmp", "image/bmp");
|
MIMETYPES.put(".bmp", "image/bmp");
|
||||||
MIMETYPES.put(".tiff", "image/tiff");
|
MIMETYPES.put(".tiff", "image/tiff");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void deleteDirRecursive(Path startDir) throws IOException {
|
||||||
|
Files.walkFileTree(startDir, new SimpleFileVisitor<>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
|
Files.delete(file);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||||
|
Files.delete(dir);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,7 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -86,6 +83,16 @@ public class Profile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<String> getAvailableProfiles() {
|
||||||
|
try (var files = Files.list(PerfinApp.APP_DIR)) {
|
||||||
|
return files.filter(Files::isDirectory)
|
||||||
|
.map(path -> path.getFileName().toString())
|
||||||
|
.sorted().toList();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String getLastProfile() {
|
public static String getLastProfile() {
|
||||||
Path lastProfileFile = PerfinApp.APP_DIR.resolve("last-profile.txt");
|
Path lastProfileFile = PerfinApp.APP_DIR.resolve("last-profile.txt");
|
||||||
if (Files.exists(lastProfileFile)) {
|
if (Files.exists(lastProfileFile)) {
|
||||||
|
@ -127,7 +134,6 @@ public class Profile {
|
||||||
for (var c : profileLoadListeners) {
|
for (var c : profileLoadListeners) {
|
||||||
c.accept(current);
|
c.accept(current);
|
||||||
}
|
}
|
||||||
profileLoadListeners.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void initProfileDir(String name) throws IOException {
|
private static void initProfileDir(String name) throws IOException {
|
||||||
|
@ -181,6 +187,13 @@ public class Profile {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean validateName(String name) {
|
public static boolean validateName(String name) {
|
||||||
return name.matches("\\w+");
|
return name != null &&
|
||||||
|
name.matches("\\w+") &&
|
||||||
|
name.toLowerCase().equals(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.andrewlalis.perfin.view;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.SceneUtil;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stage that shows a popup for interacting with Perfin's collection of
|
||||||
|
* profiles.
|
||||||
|
*/
|
||||||
|
public class ProfilesStage extends Stage {
|
||||||
|
private static ProfilesStage instance;
|
||||||
|
|
||||||
|
public ProfilesStage() {
|
||||||
|
setTitle("Profiles");
|
||||||
|
setAlwaysOnTop(false);
|
||||||
|
initModality(Modality.APPLICATION_MODAL);
|
||||||
|
setScene(SceneUtil.load("/profiles-view.fxml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void open(Window owner) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new ProfilesStage();
|
||||||
|
instance.initOwner(owner);
|
||||||
|
instance.show();
|
||||||
|
instance.setOnCloseRequest(event -> instance = null);
|
||||||
|
} else {
|
||||||
|
instance.requestFocus();
|
||||||
|
instance.toFront();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void closeView() {
|
||||||
|
if (instance != null) {
|
||||||
|
instance.close();
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
<Button text="Forward" onAction="#goForward"/>
|
<Button text="Forward" onAction="#goForward"/>
|
||||||
<Button text="Accounts" onAction="#goToAccounts"/>
|
<Button text="Accounts" onAction="#goToAccounts"/>
|
||||||
<Button text="Transactions" onAction="#goToTransactions"/>
|
<Button text="Transactions" onAction="#goToTransactions"/>
|
||||||
|
<Button text="Profiles" onAction="#viewProfiles"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
<HBox fx:id="breadcrumbHBox" styleClass="std-spacing,small-text"/>
|
<HBox fx:id="breadcrumbHBox" styleClass="std-spacing,small-text"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ScrollPane?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.scene.text.*?>
|
||||||
|
<BorderPane xmlns="http://javafx.com/javafx"
|
||||||
|
xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="com.andrewlalis.perfin.control.ProfilesViewController"
|
||||||
|
stylesheets="@style/base.css"
|
||||||
|
prefWidth="500"
|
||||||
|
prefHeight="400"
|
||||||
|
>
|
||||||
|
<top>
|
||||||
|
<VBox styleClass="std-padding,std-spacing">
|
||||||
|
<TextFlow>
|
||||||
|
<Text text="In Perfin, all your accounts, transactions, files, and other financial data are stored in a single "/>
|
||||||
|
<Text text="profile" styleClass="bold-text"/>
|
||||||
|
<Text text=". By default, Perfin uses the "/>
|
||||||
|
<Text text="default" style="-fx-font-style: italic;"/>
|
||||||
|
<Text text=" profile, and this should be sufficient for most users, but you can also add new profiles if you'd like to track some finances separately."/>
|
||||||
|
</TextFlow>
|
||||||
|
</VBox>
|
||||||
|
</top>
|
||||||
|
<center>
|
||||||
|
<ScrollPane fitToWidth="true" fitToHeight="true">
|
||||||
|
<VBox fx:id="profilesVBox" styleClass="std-padding,spacing-extra"/>
|
||||||
|
</ScrollPane>
|
||||||
|
</center>
|
||||||
|
<bottom>
|
||||||
|
<BorderPane>
|
||||||
|
<left>
|
||||||
|
<VBox styleClass="std-padding">
|
||||||
|
<Label text="Add New Profile"/>
|
||||||
|
</VBox>
|
||||||
|
</left>
|
||||||
|
<center>
|
||||||
|
<VBox styleClass="std-padding">
|
||||||
|
<TextField fx:id="newProfileNameField" style="-fx-min-width: 50px; -fx-pref-width: 50px;"/>
|
||||||
|
<Text
|
||||||
|
fx:id="newProfileNameErrorLabel"
|
||||||
|
styleClass="error-text"
|
||||||
|
style="-fx-fill: red;"
|
||||||
|
text="Invalid profile name. Profile names must only contain lowercase text."
|
||||||
|
/>
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
<right>
|
||||||
|
<VBox styleClass="std-padding">
|
||||||
|
<Button text="Add" onAction="#addProfile" fx:id="addProfileButton"/>
|
||||||
|
</VBox>
|
||||||
|
</right>
|
||||||
|
</BorderPane>
|
||||||
|
</bottom>
|
||||||
|
</BorderPane>
|
Loading…
Reference in New Issue