Added validation, cleaned up CSS colors with theme definitions.
This commit is contained in:
parent
ebf4880297
commit
c02e5d3fc6
|
@ -8,9 +8,15 @@ import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
import com.andrewlalis.perfin.view.AccountComboBoxCellFactory;
|
import com.andrewlalis.perfin.view.AccountComboBoxCellFactory;
|
||||||
import com.andrewlalis.perfin.view.component.FileSelectionArea;
|
import com.andrewlalis.perfin.view.component.FileSelectionArea;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.validators.CurrencyAmountValidator;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
@ -22,50 +28,58 @@ import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
public class CreateTransactionController implements RouteSelectionListener {
|
public class CreateTransactionController implements RouteSelectionListener {
|
||||||
@FXML public TextField timestampField;
|
@FXML public TextField timestampField;
|
||||||
@FXML public Label timestampInvalidLabel;
|
|
||||||
@FXML public Label timestampFutureLabel;
|
|
||||||
|
|
||||||
@FXML public TextField amountField;
|
@FXML public TextField amountField;
|
||||||
@FXML public ChoiceBox<Currency> currencyChoiceBox;
|
@FXML public ChoiceBox<Currency> currencyChoiceBox;
|
||||||
@FXML public TextArea descriptionField;
|
@FXML public TextArea descriptionField;
|
||||||
@FXML public Label descriptionErrorLabel;
|
|
||||||
|
|
||||||
|
@FXML public HBox linkedAccountsContainer;
|
||||||
@FXML public ComboBox<Account> linkDebitAccountComboBox;
|
@FXML public ComboBox<Account> linkDebitAccountComboBox;
|
||||||
@FXML public ComboBox<Account> linkCreditAccountComboBox;
|
@FXML public ComboBox<Account> linkCreditAccountComboBox;
|
||||||
@FXML public Label linkedAccountsErrorLabel;
|
|
||||||
|
|
||||||
@FXML public VBox attachmentsVBox;
|
@FXML public VBox attachmentsVBox;
|
||||||
private FileSelectionArea attachmentsSelectionArea;
|
private FileSelectionArea attachmentsSelectionArea;
|
||||||
|
|
||||||
|
@FXML public Button saveButton;
|
||||||
|
|
||||||
|
public CreateTransactionController() {
|
||||||
|
}
|
||||||
|
|
||||||
@FXML public void initialize() {
|
@FXML public void initialize() {
|
||||||
// Setup error field validation.
|
// Setup error field validation.
|
||||||
timestampInvalidLabel.managedProperty().bind(timestampInvalidLabel.visibleProperty());
|
var timestampValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||||
timestampFutureLabel.managedProperty().bind(timestampFutureLabel.visibleProperty());
|
.addTerminalPredicate(s -> parseTimestamp() != null, "Invalid timestamp.")
|
||||||
timestampField.textProperty().addListener((observable, oldValue, newValue) -> {
|
.addPredicate(s -> {
|
||||||
LocalDateTime parsedTimestamp = parseTimestamp();
|
LocalDateTime ts = parseTimestamp();
|
||||||
timestampInvalidLabel.setVisible(parsedTimestamp == null);
|
return ts != null && ts.isBefore(LocalDateTime.now());
|
||||||
timestampFutureLabel.setVisible(parsedTimestamp != null && parsedTimestamp.isAfter(LocalDateTime.now()));
|
}, "Timestamp cannot be in the future.")
|
||||||
});
|
).validatedInitially().attachToTextField(timestampField);
|
||||||
descriptionErrorLabel.managedProperty().bind(descriptionErrorLabel.visibleProperty());
|
|
||||||
descriptionErrorLabel.visibleProperty().bind(descriptionErrorLabel.textProperty().isNotEmpty());
|
|
||||||
descriptionField.textProperty().addListener((observable, oldValue, newValue) -> {
|
|
||||||
if (newValue != null && newValue.length() > 255) {
|
|
||||||
descriptionErrorLabel.setText("Description is too long.");
|
|
||||||
} else {
|
|
||||||
descriptionErrorLabel.setText(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
linkedAccountsErrorLabel.managedProperty().bind(linkedAccountsErrorLabel.visibleProperty());
|
|
||||||
linkedAccountsErrorLabel.visibleProperty().bind(linkedAccountsErrorLabel.textProperty().isNotEmpty());
|
|
||||||
linkDebitAccountComboBox.valueProperty().addListener((observable, oldValue, newValue) -> onLinkedAccountsUpdated());
|
|
||||||
linkCreditAccountComboBox.valueProperty().addListener((observable, oldValue, newValue) -> onLinkedAccountsUpdated());
|
|
||||||
|
|
||||||
|
var amountValid = new ValidationApplier<>(
|
||||||
|
new CurrencyAmountValidator(() -> currencyChoiceBox.getValue(), false, false)
|
||||||
|
).validatedInitially().attachToTextField(amountField, currencyChoiceBox.valueProperty());
|
||||||
|
|
||||||
|
var descriptionValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||||
|
.addTerminalPredicate(s -> s == null || s.length() <= 255, "Description is too long.")
|
||||||
|
).validatedInitially().attach(descriptionField, descriptionField.textProperty());
|
||||||
|
|
||||||
|
Property<CreditAndDebitAccounts> linkedAccountsProperty = new SimpleObjectProperty<>(getSelectedAccounts());
|
||||||
|
linkDebitAccountComboBox.valueProperty().addListener((observable, oldValue, newValue) -> linkedAccountsProperty.setValue(getSelectedAccounts()));
|
||||||
|
linkCreditAccountComboBox.valueProperty().addListener((observable, oldValue, newValue) -> linkedAccountsProperty.setValue(getSelectedAccounts()));
|
||||||
|
var linkedAccountsValid = new ValidationApplier<>(new PredicateValidator<CreditAndDebitAccounts>()
|
||||||
|
.addPredicate(accounts -> accounts.hasCredit() || accounts.hasDebit(), "At least one account must be linked.")
|
||||||
|
.addPredicate(
|
||||||
|
accounts -> (!accounts.hasCredit() || !accounts.hasDebit()) || !accounts.creditAccount().equals(accounts.debitAccount()),
|
||||||
|
"The credit and debit accounts cannot be the same."
|
||||||
|
)
|
||||||
|
).validatedInitially().attach(linkedAccountsContainer, linkedAccountsProperty);
|
||||||
|
|
||||||
|
var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
|
||||||
|
saveButton.disableProperty().bind(formValid.not());
|
||||||
|
|
||||||
// Update the lists of accounts available for linking based on the selected currency.
|
// Update the lists of accounts available for linking based on the selected currency.
|
||||||
var cellFactory = new AccountComboBoxCellFactory();
|
var cellFactory = new AccountComboBoxCellFactory();
|
||||||
|
@ -87,35 +101,23 @@ public class CreateTransactionController implements RouteSelectionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML public void save() {
|
@FXML public void save() {
|
||||||
var validationMessages = validateFormData();
|
LocalDateTime utcTimestamp = DateUtil.localToUTC(parseTimestamp());
|
||||||
if (!validationMessages.isEmpty()) {
|
BigDecimal amount = new BigDecimal(amountField.getText());
|
||||||
Alert alert = new Alert(
|
Currency currency = currencyChoiceBox.getValue();
|
||||||
Alert.AlertType.WARNING,
|
String description = descriptionField.getText() == null ? null : descriptionField.getText().strip();
|
||||||
"There are some issues with your data:\n\n" +
|
CreditAndDebitAccounts linkedAccounts = getSelectedAccounts();
|
||||||
validationMessages.stream()
|
List<Path> attachments = attachmentsSelectionArea.getSelectedFiles();
|
||||||
.map(s -> "- " + s)
|
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
|
||||||
.collect(Collectors.joining("\n\n"))
|
repo.insert(
|
||||||
|
utcTimestamp,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
description,
|
||||||
|
linkedAccounts,
|
||||||
|
attachments
|
||||||
);
|
);
|
||||||
alert.show();
|
});
|
||||||
} else {
|
router.navigateBackAndClear();
|
||||||
LocalDateTime utcTimestamp = DateUtil.localToUTC(parseTimestamp());
|
|
||||||
BigDecimal amount = new BigDecimal(amountField.getText());
|
|
||||||
Currency currency = currencyChoiceBox.getValue();
|
|
||||||
String description = descriptionField.getText() == null ? null : descriptionField.getText().strip();
|
|
||||||
CreditAndDebitAccounts linkedAccounts = getSelectedAccounts();
|
|
||||||
List<Path> attachments = attachmentsSelectionArea.getSelectedFiles();
|
|
||||||
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
|
|
||||||
repo.insert(
|
|
||||||
utcTimestamp,
|
|
||||||
amount,
|
|
||||||
currency,
|
|
||||||
description,
|
|
||||||
linkedAccounts,
|
|
||||||
attachments
|
|
||||||
);
|
|
||||||
});
|
|
||||||
router.navigateBackAndClear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML public void cancel() {
|
@FXML public void cancel() {
|
||||||
|
@ -129,7 +131,7 @@ public class CreateTransactionController implements RouteSelectionListener {
|
||||||
|
|
||||||
private void resetForm() {
|
private void resetForm() {
|
||||||
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
|
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
|
||||||
amountField.setText("0");
|
amountField.setText(null);
|
||||||
descriptionField.setText(null);
|
descriptionField.setText(null);
|
||||||
attachmentsSelectionArea.clear();
|
attachmentsSelectionArea.clear();
|
||||||
Thread.ofVirtual().start(() -> {
|
Thread.ofVirtual().start(() -> {
|
||||||
|
@ -191,41 +193,4 @@ public class CreateTransactionController implements RouteSelectionListener {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onLinkedAccountsUpdated() {
|
|
||||||
Account debitAccount = linkDebitAccountComboBox.getValue();
|
|
||||||
Account creditAccount = linkCreditAccountComboBox.getValue();
|
|
||||||
if (debitAccount == null && creditAccount == null) {
|
|
||||||
linkedAccountsErrorLabel.setText("At least one credit or debit account must be linked to the transaction for it to have any effect.");
|
|
||||||
} else if (debitAccount != null && debitAccount.equals(creditAccount)) {
|
|
||||||
linkedAccountsErrorLabel.setText("Cannot link the same account to both credit and debit.");
|
|
||||||
} else {
|
|
||||||
linkedAccountsErrorLabel.setText(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> validateFormData() {
|
|
||||||
List<String> errorMessages = new ArrayList<>();
|
|
||||||
if (parseTimestamp() == null) errorMessages.add("Invalid or missing timestamp.");
|
|
||||||
if (descriptionField.getText() != null && descriptionField.getText().strip().length() > 255) {
|
|
||||||
errorMessages.add("Description is too long.");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
BigDecimal value = new BigDecimal(amountField.getText());
|
|
||||||
if (value.compareTo(BigDecimal.ZERO) <= 0) {
|
|
||||||
errorMessages.add("Amount should be a positive number.");
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
errorMessages.add("Invalid or missing amount.");
|
|
||||||
}
|
|
||||||
Account debitAccount = linkDebitAccountComboBox.getValue();
|
|
||||||
Account creditAccount = linkCreditAccountComboBox.getValue();
|
|
||||||
if (debitAccount == null && creditAccount == null) {
|
|
||||||
errorMessages.add("At least one account must be linked to this transaction.");
|
|
||||||
}
|
|
||||||
if (debitAccount != null && debitAccount.equals(creditAccount)) {
|
|
||||||
errorMessages.add("Credit and debit accounts cannot be the same.");
|
|
||||||
}
|
|
||||||
return errorMessages;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,15 @@ import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||||
import com.andrewlalis.perfin.model.Account;
|
import com.andrewlalis.perfin.model.Account;
|
||||||
import com.andrewlalis.perfin.model.AccountType;
|
import com.andrewlalis.perfin.model.AccountType;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
|
import com.andrewlalis.perfin.view.component.PropertiesPane;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.validators.CurrencyAmountValidator;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
|
||||||
|
import javafx.beans.binding.BooleanExpression;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.ChoiceBox;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.ComboBox;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -41,12 +42,33 @@ public class EditAccountController implements RouteSelectionListener {
|
||||||
@FXML
|
@FXML
|
||||||
public ChoiceBox<AccountType> accountTypeChoiceBox;
|
public ChoiceBox<AccountType> accountTypeChoiceBox;
|
||||||
@FXML
|
@FXML
|
||||||
public VBox initialBalanceContent;
|
public PropertiesPane initialBalanceContent;
|
||||||
@FXML
|
@FXML
|
||||||
public TextField initialBalanceField;
|
public TextField initialBalanceField;
|
||||||
|
|
||||||
|
@FXML public Button saveButton;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
var nameValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||||
|
.addTerminalPredicate(s -> s != null && !s.isBlank(), "Name should not be empty.")
|
||||||
|
.addPredicate(s -> s.length() <= 63, "Name is too long.")
|
||||||
|
).attachToTextField(accountNameField);
|
||||||
|
|
||||||
|
var numberValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||||
|
.addTerminalPredicate(s -> s != null && !s.isBlank(), "Account number should not be empty.")
|
||||||
|
.addPredicate(s -> s.length() <= 255, "Account number is too long.")
|
||||||
|
.addPredicate(s -> s.matches("\\d+"), "Account number should contain only numeric digits.")
|
||||||
|
).attachToTextField(accountNumberField);
|
||||||
|
|
||||||
|
var balanceValid = new ValidationApplier<>(
|
||||||
|
new CurrencyAmountValidator(() -> accountCurrencyComboBox.getValue(), false, false)
|
||||||
|
).attachToTextField(initialBalanceField, accountCurrencyComboBox.valueProperty());
|
||||||
|
|
||||||
|
// Combine validity of all fields for an expression that determines if the whole form is valid.
|
||||||
|
BooleanExpression formValid = nameValid.and(numberValid).and(balanceValid);
|
||||||
|
saveButton.disableProperty().bind(formValid.not());
|
||||||
|
|
||||||
List<Currency> priorityCurrencies = Stream.of("USD", "EUR", "GBP", "CAD", "AUD")
|
List<Currency> priorityCurrencies = Stream.of("USD", "EUR", "GBP", "CAD", "AUD")
|
||||||
.map(Currency::getInstance)
|
.map(Currency::getInstance)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.andrewlalis.perfin.view.component.validation;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.decorators.FieldSubtextDecorator;
|
||||||
|
import javafx.beans.binding.BooleanExpression;
|
||||||
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fluent interface for applying a validator to one or more controls.
|
||||||
|
* @param <T> The value type.
|
||||||
|
*/
|
||||||
|
public class ValidationApplier<T> {
|
||||||
|
private final ValidationFunction<T> validator;
|
||||||
|
private ValidationDecorator decorator = new FieldSubtextDecorator();
|
||||||
|
private boolean validateInitially = false;
|
||||||
|
|
||||||
|
public ValidationApplier(ValidationFunction<T> validator) {
|
||||||
|
this.validator = validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationApplier<T> decoratedWith(ValidationDecorator decorator) {
|
||||||
|
this.decorator = decorator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationApplier<T> validatedInitially() {
|
||||||
|
this.validateInitially = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanExpression attach(Node node, Property<T> valueProperty, Property<?>... triggerProperties) {
|
||||||
|
BooleanExpression validProperty = BooleanExpression.booleanExpression(
|
||||||
|
valueProperty.map(value -> validator.validate(value).isValid())
|
||||||
|
);
|
||||||
|
valueProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
ValidationResult result = validator.validate(newValue);
|
||||||
|
decorator.decorate(node, result);
|
||||||
|
});
|
||||||
|
for (Property<?> influencingProperty : triggerProperties) {
|
||||||
|
influencingProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
ValidationResult result = validator.validate(valueProperty.getValue());
|
||||||
|
decorator.decorate(node, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateInitially) {
|
||||||
|
// Call the decorator once to perform validation right away.
|
||||||
|
decorator.decorate(node, validator.validate(valueProperty.getValue()));
|
||||||
|
}
|
||||||
|
return validProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public BooleanExpression attachToTextField(TextField field, Property<?>... triggerProperties) {
|
||||||
|
return attach(field, (Property<T>) field.textProperty(), triggerProperties);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.andrewlalis.perfin.view.component.validation;
|
||||||
|
|
||||||
|
import javafx.scene.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A controller style component that, when validation of a field updates,
|
||||||
|
* will represent the result in some way in the UI, usually by modifying the
|
||||||
|
* given control and/or its parents in the scene graph.
|
||||||
|
*/
|
||||||
|
public interface ValidationDecorator {
|
||||||
|
void decorate(Node node, ValidationResult result);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.andrewlalis.perfin.view.component.validation;
|
||||||
|
|
||||||
|
public interface ValidationFunction<T> {
|
||||||
|
ValidationResult validate(T input);
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.andrewlalis.perfin.view.component.validation;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public record ValidationResult(List<String> messages) {
|
||||||
|
public boolean isValid() {
|
||||||
|
return messages.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String asLines() {
|
||||||
|
return messages.stream().map(String::strip).collect(Collectors.joining("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ValidationResult valid() {
|
||||||
|
return new ValidationResult(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ValidationResult of(String... messages) {
|
||||||
|
return new ValidationResult(List.of(messages));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.andrewlalis.perfin.view.component.validation.decorators;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.ValidationDecorator;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.ValidationResult;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A validation decorator that can be applied to most controls, such that when
|
||||||
|
* the field's value is invalid, messages are displayed in a label below the
|
||||||
|
* field. This is accomplished by wrapping the control in a VBox only while
|
||||||
|
* the value is invalid.
|
||||||
|
*/
|
||||||
|
public class FieldSubtextDecorator implements ValidationDecorator {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(FieldSubtextDecorator.class);
|
||||||
|
private static final String WRAP_KEY = FieldSubtextDecorator.class.getName();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void decorate(Node node, ValidationResult result) {
|
||||||
|
if (!result.isValid()) {
|
||||||
|
if (!node.getStyleClass().contains("validation-field-invalid")) {
|
||||||
|
node.getStyleClass().add("validation-field-invalid");
|
||||||
|
}
|
||||||
|
// Wrap the control in a VBox and put error messages under it.
|
||||||
|
Label errorLabel;
|
||||||
|
if (isNodeWrapped(node)) {
|
||||||
|
VBox validationContainer = (VBox) node.getParent();
|
||||||
|
errorLabel = (Label) validationContainer.getChildren().get(1);
|
||||||
|
} else {
|
||||||
|
errorLabel = wrapNode(node);
|
||||||
|
}
|
||||||
|
errorLabel.setText(result.asLines());
|
||||||
|
} else {
|
||||||
|
node.getStyleClass().remove("validation-field-invalid");
|
||||||
|
// Unwrap the control, if it's wrapped.
|
||||||
|
if (isNodeWrapped(node)) {
|
||||||
|
unwrapNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isNodeWrapped(Node node) {
|
||||||
|
return WRAP_KEY.equals(node.getParent().getUserData());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Label wrapNode(Node node) {
|
||||||
|
Pane trueParent = (Pane) node.getParent();
|
||||||
|
int idx = trueParent.getChildren().indexOf(node);
|
||||||
|
|
||||||
|
Label errorLabel = new Label();
|
||||||
|
errorLabel.getStyleClass().addAll("small-font", "negative-color-text-fill");
|
||||||
|
errorLabel.setWrapText(true);
|
||||||
|
VBox validationContainer = new VBox(node, errorLabel);
|
||||||
|
validationContainer.setUserData(WRAP_KEY);
|
||||||
|
trueParent.getChildren().add(idx, validationContainer);
|
||||||
|
return errorLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unwrapNode(Node node) {
|
||||||
|
VBox validationContainer = (VBox) node.getParent();
|
||||||
|
Pane trueParent = (Pane) node.getParent().getParent();
|
||||||
|
int idx = trueParent.getChildren().indexOf(validationContainer);
|
||||||
|
trueParent.getChildren().remove(idx);
|
||||||
|
trueParent.getChildren().add(idx, node);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.andrewlalis.perfin.view.component.validation.validators;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.ValidationFunction;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.ValidationResult;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Currency;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class CurrencyAmountValidator implements ValidationFunction<String> {
|
||||||
|
private final Supplier<Currency> currencySupplier;
|
||||||
|
private final boolean allowNegative;
|
||||||
|
private final boolean allowEmpty;
|
||||||
|
|
||||||
|
public CurrencyAmountValidator(Supplier<Currency> currencySupplier, boolean allowNegative, boolean allowEmpty) {
|
||||||
|
this.currencySupplier = currencySupplier;
|
||||||
|
this.allowNegative = allowNegative;
|
||||||
|
this.allowEmpty = allowEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationResult validate(String input) {
|
||||||
|
if (input == null || input.isBlank()) {
|
||||||
|
if (!allowEmpty) {
|
||||||
|
return ValidationResult.of("Amount should not be empty.");
|
||||||
|
}
|
||||||
|
return ValidationResult.valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BigDecimal amount = new BigDecimal(input);
|
||||||
|
int scale = amount.scale();
|
||||||
|
List<String> messages = new ArrayList<>();
|
||||||
|
Currency currency = currencySupplier.get();
|
||||||
|
if (currency != null && scale > currency.getDefaultFractionDigits()) {
|
||||||
|
messages.add("The selected currency doesn't support that many digits.");
|
||||||
|
}
|
||||||
|
if (!allowNegative && amount.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
messages.add("Negative amounts are not allowed.");
|
||||||
|
}
|
||||||
|
return new ValidationResult(messages);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return ValidationResult.of("Invalid amount. Should be a decimal value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.andrewlalis.perfin.view.component.validation.validators;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.ValidationFunction;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.ValidationResult;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common validator pattern, where a series of checks are done on a value to
|
||||||
|
* determine if it's valid. If invalid, a message is added.
|
||||||
|
* @param <T> The value type.
|
||||||
|
*/
|
||||||
|
public class PredicateValidator<T> implements ValidationFunction<T> {
|
||||||
|
private record ValidationStep<T>(Function<T, Boolean> predicate, String message, boolean terminal) {}
|
||||||
|
|
||||||
|
private final List<ValidationStep<T>> steps = new ArrayList<>();
|
||||||
|
|
||||||
|
public PredicateValidator<T> addPredicate(Function<T, Boolean> predicate, String errorMessage) {
|
||||||
|
steps.add(new ValidationStep<>(predicate, errorMessage, false));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PredicateValidator<T> addTerminalPredicate(Function<T, Boolean> predicate, String errorMessage) {
|
||||||
|
steps.add(new ValidationStep<>(predicate, errorMessage, true));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationResult validate(T input) {
|
||||||
|
List<String> messages = new ArrayList<>();
|
||||||
|
for (var step : steps) {
|
||||||
|
if (!step.predicate().apply(input)) {
|
||||||
|
messages.add(step.message());
|
||||||
|
if (step.terminal()) {
|
||||||
|
return new ValidationResult(messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ValidationResult(messages);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,4 +18,5 @@ module com.andrewlalis.perfin {
|
||||||
opens com.andrewlalis.perfin.control to javafx.fxml;
|
opens com.andrewlalis.perfin.control to javafx.fxml;
|
||||||
opens com.andrewlalis.perfin.view to javafx.fxml;
|
opens com.andrewlalis.perfin.view to javafx.fxml;
|
||||||
opens com.andrewlalis.perfin.view.component to javafx.fxml;
|
opens com.andrewlalis.perfin.view.component to javafx.fxml;
|
||||||
|
opens com.andrewlalis.perfin.view.component.validation to javafx.fxml;
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
|
||||||
<?import javafx.scene.control.*?>
|
<?import javafx.scene.control.*?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
<BorderPane xmlns="http://javafx.com/javafx"
|
<BorderPane xmlns="http://javafx.com/javafx"
|
||||||
|
@ -9,64 +10,61 @@
|
||||||
>
|
>
|
||||||
<center>
|
<center>
|
||||||
<ScrollPane fitToWidth="true" fitToHeight="true">
|
<ScrollPane fitToWidth="true" fitToHeight="true">
|
||||||
<VBox styleClass="std-spacing,std-padding" style="-fx-max-width: 500px;">
|
<VBox style="-fx-max-width: 400px;">
|
||||||
<!-- Basic properties -->
|
<!-- Basic properties -->
|
||||||
<VBox>
|
<PropertiesPane hgap="5" vgap="5" styleClass="std-padding">
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints hgrow="NEVER" halignment="LEFT" minWidth="150"/>
|
||||||
|
<ColumnConstraints hgrow="ALWAYS" halignment="RIGHT"/>
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
<Label text="Timestamp" labelFor="${timestampField}" styleClass="bold-text"/>
|
<Label text="Timestamp" labelFor="${timestampField}" styleClass="bold-text"/>
|
||||||
<TextField fx:id="timestampField" styleClass="mono-font"/>
|
<TextField fx:id="timestampField" styleClass="mono-font"/>
|
||||||
<Label fx:id="timestampInvalidLabel" text="Invalid timestamp." styleClass="error-text"/>
|
|
||||||
<Label fx:id="timestampFutureLabel" text="Timestamp cannot be in the future." styleClass="error-text"/>
|
|
||||||
</VBox>
|
|
||||||
<VBox>
|
|
||||||
<Label text="Amount" labelFor="${amountField}" styleClass="bold-text"/>
|
<Label text="Amount" labelFor="${amountField}" styleClass="bold-text"/>
|
||||||
<TextField fx:id="amountField" styleClass="mono-font"/>
|
<TextField fx:id="amountField" styleClass="mono-font"/>
|
||||||
</VBox>
|
|
||||||
<VBox>
|
|
||||||
<Label text="Currency" labelFor="${currencyChoiceBox}" styleClass="bold-text"/>
|
<Label text="Currency" labelFor="${currencyChoiceBox}" styleClass="bold-text"/>
|
||||||
<ChoiceBox fx:id="currencyChoiceBox"/>
|
<ChoiceBox fx:id="currencyChoiceBox"/>
|
||||||
</VBox>
|
|
||||||
<VBox>
|
|
||||||
<Label text="Description" labelFor="${descriptionField}" styleClass="bold-text"/>
|
<Label text="Description" labelFor="${descriptionField}" styleClass="bold-text"/>
|
||||||
<Label text="Maximum of 255 characters." styleClass="small-font"/>
|
|
||||||
<TextArea
|
<TextArea
|
||||||
fx:id="descriptionField"
|
fx:id="descriptionField"
|
||||||
styleClass="mono-font"
|
styleClass="mono-font"
|
||||||
wrapText="true"
|
wrapText="true"
|
||||||
style="-fx-pref-height: 100px;-fx-min-height: 100px;"
|
style="-fx-pref-height: 100px;-fx-min-height: 100px;"
|
||||||
/>
|
/>
|
||||||
<Label fx:id="descriptionErrorLabel" styleClass="error-text" wrapText="true"/>
|
</PropertiesPane>
|
||||||
</VBox>
|
|
||||||
<!-- Container for linked accounts -->
|
<!-- Container for linked accounts -->
|
||||||
<VBox>
|
<HBox styleClass="std-padding,std-spacing" fx:id="linkedAccountsContainer">
|
||||||
<HBox styleClass="std-spacing">
|
<VBox>
|
||||||
<VBox>
|
<Label text="Debited Account" labelFor="${linkDebitAccountComboBox}" styleClass="bold-text"/>
|
||||||
<Label text="Debited Account" labelFor="${linkDebitAccountComboBox}" styleClass="bold-text"/>
|
<ComboBox fx:id="linkDebitAccountComboBox">
|
||||||
<ComboBox fx:id="linkDebitAccountComboBox">
|
<tooltip><Tooltip text="The account whose assets will increase as a result of this transaction."/></tooltip>
|
||||||
<tooltip><Tooltip text="The account whose assets will increase as a result of this transaction."/></tooltip>
|
</ComboBox>
|
||||||
</ComboBox>
|
</VBox>
|
||||||
</VBox>
|
<VBox>
|
||||||
<VBox>
|
<Label text="Credited Account" labelFor="${linkCreditAccountComboBox}" styleClass="bold-text"/>
|
||||||
<Label text="Credited Account" labelFor="${linkCreditAccountComboBox}" styleClass="bold-text"/>
|
<ComboBox fx:id="linkCreditAccountComboBox">
|
||||||
<ComboBox fx:id="linkCreditAccountComboBox">
|
<tooltip><Tooltip text="The account whose assets will decrease as a result of this transaction."/></tooltip>
|
||||||
<tooltip><Tooltip text="The account whose assets will decrease as a result of this transaction."/></tooltip>
|
</ComboBox>
|
||||||
</ComboBox>
|
</VBox>
|
||||||
</VBox>
|
</HBox>
|
||||||
</HBox>
|
|
||||||
<Label fx:id="linkedAccountsErrorLabel" styleClass="error-text" wrapText="true"/>
|
|
||||||
</VBox>
|
|
||||||
<!-- Container for attachments -->
|
<!-- Container for attachments -->
|
||||||
<VBox fx:id="attachmentsVBox">
|
<VBox fx:id="attachmentsVBox" styleClass="std-padding">
|
||||||
<Label text="Attachments" styleClass="bold-text"/>
|
<Label text="Attachments" styleClass="bold-text"/>
|
||||||
<Label text="Attach receipts, invoices, or other content to this transaction." styleClass="small-font" wrapText="true"/>
|
<Label text="Attach receipts, invoices, or other content to this transaction." styleClass="small-font" wrapText="true"/>
|
||||||
<!-- FileSelectionArea inserted here! -->
|
<!-- FileSelectionArea inserted here! -->
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<Separator/>
|
||||||
|
<HBox styleClass="std-padding,std-spacing" alignment="CENTER_RIGHT">
|
||||||
|
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
||||||
|
<Button text="Cancel" onAction="#cancel"/>
|
||||||
|
</HBox>
|
||||||
</VBox>
|
</VBox>
|
||||||
</ScrollPane>
|
</ScrollPane>
|
||||||
</center>
|
</center>
|
||||||
<bottom>
|
|
||||||
<HBox styleClass="std-padding,std-spacing">
|
|
||||||
<Button text="Save" onAction="#save"/>
|
|
||||||
<Button text="Cancel" onAction="#cancel"/>
|
|
||||||
</HBox>
|
|
||||||
</bottom>
|
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
|
||||||
<?import javafx.scene.control.*?>
|
<?import javafx.scene.control.*?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
<BorderPane
|
<BorderPane
|
||||||
xmlns="http://javafx.com/javafx/17.0.2-ea"
|
xmlns="http://javafx.com/javafx/17.0.2-ea"
|
||||||
xmlns:fx="http://javafx.com/fxml/1"
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
fx:controller="com.andrewlalis.perfin.control.EditAccountController"
|
fx:controller="com.andrewlalis.perfin.control.EditAccountController"
|
||||||
stylesheets="@style/edit-account.css,@style/base.css"
|
stylesheets="@style/base.css"
|
||||||
>
|
>
|
||||||
<top>
|
<top>
|
||||||
<HBox styleClass="std-padding,std-spacing">
|
<HBox styleClass="std-padding,std-spacing">
|
||||||
|
@ -14,34 +15,42 @@
|
||||||
</HBox>
|
</HBox>
|
||||||
</top>
|
</top>
|
||||||
<center>
|
<center>
|
||||||
<GridPane BorderPane.alignment="TOP_LEFT" styleClass="fields-grid,std-padding">
|
<VBox style="-fx-max-width: 400px;" BorderPane.alignment="TOP_LEFT">
|
||||||
<columnConstraints>
|
<PropertiesPane hgap="5" vgap="5" styleClass="std-padding">
|
||||||
<ColumnConstraints hgrow="NEVER" minWidth="10.0" />
|
<columnConstraints>
|
||||||
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" />
|
<ColumnConstraints hgrow="NEVER" halignment="LEFT" minWidth="150"/>
|
||||||
</columnConstraints>
|
<ColumnConstraints hgrow="ALWAYS" halignment="RIGHT"/>
|
||||||
<Label GridPane.columnIndex="0" GridPane.rowIndex="0" text="Name"/>
|
</columnConstraints>
|
||||||
<TextField fx:id="accountNameField" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
|
|
||||||
|
|
||||||
<Label GridPane.columnIndex="0" GridPane.rowIndex="1" text="Account Number"/>
|
<Label text="Name" styleClass="bold-text"/>
|
||||||
<TextField fx:id="accountNumberField" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
|
<TextField fx:id="accountNameField"/>
|
||||||
|
|
||||||
<Label GridPane.columnIndex="0" GridPane.rowIndex="2" text="Currency"/>
|
<Label text="Account Number" styleClass="bold-text"/>
|
||||||
<ComboBox fx:id="accountCurrencyComboBox" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
|
<TextField fx:id="accountNumberField" styleClass="mono-font"/>
|
||||||
|
|
||||||
<Label GridPane.columnIndex="0" GridPane.rowIndex="3" text="Account Type"/>
|
<Label text="Currency" styleClass="bold-text"/>
|
||||||
<ChoiceBox fx:id="accountTypeChoiceBox" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
|
<ComboBox fx:id="accountCurrencyComboBox"/>
|
||||||
|
|
||||||
|
<Label text="Account Type" styleClass="bold-text"/>
|
||||||
|
<ChoiceBox fx:id="accountTypeChoiceBox"/>
|
||||||
|
</PropertiesPane>
|
||||||
|
|
||||||
|
<!-- Initial balance content that's only visible when creating a new account. -->
|
||||||
|
<PropertiesPane fx:id="initialBalanceContent" hgap="5" vgap="5" styleClass="std-padding">
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints hgrow="NEVER" halignment="LEFT" minWidth="150"/>
|
||||||
|
<ColumnConstraints hgrow="ALWAYS" halignment="RIGHT"/>
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
<VBox fx:id="initialBalanceContent" GridPane.columnIndex="0" GridPane.rowIndex="4" GridPane.columnSpan="2">
|
|
||||||
<Separator/>
|
|
||||||
<Label text="Initial Balance" styleClass="bold-text"/>
|
<Label text="Initial Balance" styleClass="bold-text"/>
|
||||||
<TextField fx:id="initialBalanceField"/>
|
<TextField fx:id="initialBalanceField"/>
|
||||||
</VBox>
|
</PropertiesPane>
|
||||||
</GridPane>
|
|
||||||
|
<Separator/>
|
||||||
|
<HBox styleClass="std-padding,std-spacing" alignment="CENTER_RIGHT">
|
||||||
|
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
||||||
|
<Button text="Cancel" onAction="#cancel"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
</center>
|
</center>
|
||||||
<bottom>
|
|
||||||
<FlowPane BorderPane.alignment="CENTER_RIGHT">
|
|
||||||
<Button text="Save" onAction="#save"/>
|
|
||||||
<Button text="Cancel" onAction="#cancel"/>
|
|
||||||
</FlowPane>
|
|
||||||
</bottom>
|
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
|
|
@ -4,10 +4,21 @@ classes to use for common style variants. Prefer to use the styles defined here
|
||||||
rather than with your own CSS.
|
rather than with your own CSS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
-fx-theme-text: rgb(26, 26, 26);
|
||||||
|
-fx-theme-text-secondary: rgb(99, 99, 99);
|
||||||
|
-fx-theme-background: rgb(237, 237, 237);
|
||||||
|
-fx-theme-background-2: rgb(232, 232, 232);
|
||||||
|
-fx-theme-background-3: rgb(220, 220, 220);
|
||||||
|
-fx-theme-negative: rgb(247, 37, 69);
|
||||||
|
-fx-theme-positive: rgb(43, 196, 77);
|
||||||
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
-fx-font-family: "Roboto", sans-serif;
|
-fx-font-family: "Roboto", sans-serif;
|
||||||
-fx-font-size: 14px;
|
-fx-font-size: 14px;
|
||||||
-fx-text-fill: rgb(26, 26, 26);
|
-fx-text-fill: -fx-theme-text;
|
||||||
|
-fx-background-color: -fx-theme-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mono-font {
|
.mono-font {
|
||||||
|
@ -24,7 +35,7 @@ rather than with your own CSS.
|
||||||
|
|
||||||
.error-text {
|
.error-text {
|
||||||
-fx-font-size: small;
|
-fx-font-size: small;
|
||||||
-fx-text-fill: red;
|
-fx-text-fill: -fx-theme-negative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bold-text {
|
.bold-text {
|
||||||
|
@ -65,38 +76,47 @@ rather than with your own CSS.
|
||||||
|
|
||||||
/* Standard "tile" styling. */
|
/* Standard "tile" styling. */
|
||||||
.tile {
|
.tile {
|
||||||
-fx-border-color: lightgray;
|
-fx-background-color: -fx-theme-background-2;
|
||||||
-fx-border-width: 2px;
|
-fx-padding: 10px;
|
||||||
-fx-border-style: solid;
|
}
|
||||||
-fx-border-radius: 5px;
|
|
||||||
-fx-padding: 5px;
|
.tile:hover {
|
||||||
|
-fx-background-color: derive(-fx-theme-background-2, -5%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-border-selected {
|
.tile-border-selected {
|
||||||
-fx-border-color: darkgray;
|
-fx-background-color: -fx-theme-background-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validation styling. */
|
||||||
|
.validation-field-invalid {
|
||||||
|
-fx-border-color: -fx-theme-negative;
|
||||||
|
-fx-border-width: 2px;
|
||||||
|
-fx-border-style: solid;
|
||||||
|
-fx-border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Standard colors */
|
/* Standard colors */
|
||||||
.normal-color-text-fill {
|
.normal-color-text-fill {
|
||||||
-fx-text-fill: rgb(26, 26, 26);
|
-fx-text-fill: -fx-theme-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-color-fill {
|
.secondary-color-fill {
|
||||||
-fx-fill: rgb(99, 99, 99);
|
-fx-fill: -fx-theme-text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.negative-color-text-fill {
|
.negative-color-text-fill {
|
||||||
-fx-text-fill: rgb(247, 37, 69);
|
-fx-text-fill: -fx-theme-negative;
|
||||||
}
|
}
|
||||||
.negative-color-fill {
|
.negative-color-fill {
|
||||||
-fx-fill: rgb(247, 37, 69);
|
-fx-fill: -fx-theme-negative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.positive-color-text-fill {
|
.positive-color-text-fill {
|
||||||
-fx-text-fill: rgb(43, 196, 77);
|
-fx-text-fill: -fx-theme-positive;
|
||||||
}
|
}
|
||||||
.positive-color-fill {
|
.positive-color-fill {
|
||||||
-fx-fill: rgb(43, 196, 77);
|
-fx-fill: -fx-theme-positive;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* DEBUG BORDERS */
|
/* DEBUG BORDERS */
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
.fields-grid {
|
|
||||||
-fx-hgap: 3px;
|
|
||||||
-fx-vgap: 3px;
|
|
||||||
-fx-max-width: 500px;
|
|
||||||
}
|
|
Loading…
Reference in New Issue