Added help pages, styled text implementation, and improved splash screen.

This commit is contained in:
Andrew Lalis 2024-01-09 12:34:06 -05:00
parent 2a79afe1b5
commit ce78df559e
17 changed files with 592 additions and 121 deletions

View File

@ -26,9 +26,9 @@
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
inkscape:zoom="2.0863221"
inkscape:cx="143.55406"
inkscape:cy="122.22466"
inkscape:zoom="1.4752525"
inkscape:cx="125.40226"
inkscape:cy="94.899008"
inkscape:window-width="1920"
inkscape:window-height="1025"
inkscape:window-x="1080"
@ -36,6 +36,11 @@
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1"><rect
x="317.65446"
y="235.76737"
width="216.44269"
height="83.027779"
id="rect4" /><rect
x="23.509132"
y="17.511823"
width="25.696689"
@ -45,7 +50,12 @@
y="17.511823"
width="25.696689"
height="28.301114"
id="rect3" /></defs><g
id="rect3" /><rect
x="317.65446"
y="235.76737"
width="216.44269"
height="83.027779"
id="rect5" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><rect
@ -54,9 +64,26 @@
width="105.83333"
height="52.916664"
x="0"
y="0" /><g
y="0" /><path
style="display:inline;fill:#397526;fill-opacity:1;stroke:none;stroke-width:35.921;stroke-linecap:round"
d="M -51.278833,56.497593 160.38037,45.238259 159.49177,-7.633004 c 0,0 -24.42624,1.512546 -47.38917,9.245796 -22.962932,7.733249 -18.862832,17.268118 -51.065942,24.86209 -32.20311,7.593969 -45.158576,4.517345 -74.188418,14.395906 -29.029843,9.87856 -38.127073,15.626805 -38.127073,15.626805 z"
id="path5"
sodipodi:nodetypes="ccczzzc" /><path
style="display:inline;fill:#e7b300;fill-opacity:1;stroke:none;stroke-width:25.4;stroke-linecap:round"
d="M -1.3473491,68.51775 104.48598,33.263506 v -52.916667 c 0,0 -12.21534,4.282234 -23.751883,14.636677 C 69.197554,5.3379594 71.165788,14.434953 55.014288,25.695895 38.862788,36.956837 32.416445,35.338519 17.831036,48.531087 3.2456269,61.723654 -1.3473491,68.51775 -1.3473491,68.51775 Z"
id="path3"
sodipodi:nodetypes="ccczzzc" /><path
style="display:inline;fill:#ba8e00;fill-opacity:1;stroke:none;stroke-width:25.4;stroke-linecap:round"
d="M 1.2353516e-7,65.117916 105.83333,40.715416 V -12.20125 c 0,0 -12.215341,3.0297196 -23.751884,12.20125 C 70.544903,9.1715303 72.513137,18.470338 56.361637,28.075167 40.210137,37.679996 33.763794,35.400695 19.178385,47.097731 4.5929761,58.794766 1.2353516e-7,65.117916 1.2353516e-7,65.117916 Z"
id="path2"
sodipodi:nodetypes="ccczzzc" /><path
style="display:inline;fill:#ca9c00;fill-opacity:1;stroke:none;stroke-width:25.4;stroke-linecap:round"
d="M 0,52.916666 H 105.83333 V 0 c 0,0 -12.215341,0.21316952 -23.751884,6.7246636 C 70.544903,13.236158 72.513137,22.988791 56.361637,28.869491 40.210137,34.750191 33.763794,30.984525 19.178385,39.318533 4.592976,47.65254 0,52.916666 0,52.916666 Z"
id="path1"
sodipodi:nodetypes="ccczzzc" /><g
id="logo-group"
transform="translate(-30.013895,12.599684)"><path
transform="translate(41.550528,32.475407)"
style="display:inline"><path
id="top-right-background"
style="fill:#346b23;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round"
d="M 4.2333332,0 C 1.8880691,0 0,1.8880691 0,4.2333332 V 12.7 c 0,2.345264 1.8880691,4.233333 4.2333332,4.233333 H 12.7 c 2.345264,0 4.233333,-1.888069 4.233333,-4.233333 V 4.2333332 C 16.933333,1.8880691 15.045264,0 12.7,0 Z" /><path
@ -71,16 +98,22 @@
style="font-size:8px;line-height:8.64px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono';letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect3);display:inline;fill:#ca9c00;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round"
d="m 25.689419,18.184344 v 0.507817 c -0.54427,0.01823 -0.959661,0.136597 -1.246119,0.355347 -0.283854,0.216146 -0.425622,0.518229 -0.425622,0.90625 0,0.244791 0.0468,0.455806 0.14055,0.632889 0.09635,0.177084 0.24613,0.32939 0.449255,0.456994 0.205729,0.125 0.527312,0.240838 0.964812,0.347609 l 0.117124,0.03137 v 0.115242 l 0.628916,-0.628916 c -0.04352,-0.0111 -0.08269,-0.02199 -0.128837,-0.03325 v -1.664003 c 0.283854,0.01823 0.509099,0.0976 0.675766,0.238223 0.158491,0.133727 0.2669,0.329437 0.326484,0.585622 l 0.527896,-0.527896 c -0.107669,-0.217411 -0.243902,-0.385244 -0.409099,-0.503008 -0.26302,-0.190104 -0.636672,-0.294241 -1.121047,-0.312471 v -0.507817 z m 0,1.0194 v 1.613387 c -0.309896,-0.08073 -0.523441,-0.153667 -0.640628,-0.218771 -0.117188,-0.06771 -0.207095,-0.150977 -0.269595,-0.249935 -0.0599,-0.101562 -0.08973,-0.22657 -0.08973,-0.375007 0,-0.236979 0.08464,-0.420695 0.253909,-0.550903 0.16927,-0.132812 0.417915,-0.20575 0.74604,-0.218771 z"
transform="matrix(2.4707763,0,0,2.4707763,-55.488799,-44.26592)"
sodipodi:nodetypes="cccscccccccccccccccccccscsc" /></g><path
style="display:inline;fill:#e7b300;fill-opacity:1;stroke:none;stroke-width:25.4;stroke-linecap:round"
d="M -1.3473491,68.51775 104.48598,33.263506 v -52.916667 c 0,0 -12.21534,4.282234 -23.751883,14.636677 C 69.197554,5.3379594 71.165788,14.434953 55.014288,25.695895 38.862788,36.956837 32.416445,35.338519 17.831036,48.531087 3.2456269,61.723654 -1.3473491,68.51775 -1.3473491,68.51775 Z"
id="path3"
sodipodi:nodetypes="ccczzzc" /><path
style="display:inline;fill:#a78000;fill-opacity:1;stroke:none;stroke-width:25.4;stroke-linecap:round"
d="M 1.2353516e-7,65.117916 105.83333,40.715416 V -12.20125 c 0,0 -12.215341,3.0297196 -23.751884,12.20125 C 70.544903,9.1715303 72.513137,18.470338 56.361637,28.075167 40.210137,37.679996 33.763794,35.400695 19.178385,47.097731 4.5929761,58.794766 1.2353516e-7,65.117916 1.2353516e-7,65.117916 Z"
id="path2"
sodipodi:nodetypes="ccczzzc" /><path
style="display:inline;fill:#ca9c00;fill-opacity:1;stroke:none;stroke-width:25.4;stroke-linecap:round"
d="M 0,52.916666 H 105.83333 V 0 c 0,0 -12.215341,0.21316952 -23.751884,6.7246636 C 70.544903,13.236158 72.513137,22.988791 56.361637,28.869491 40.210137,34.750191 33.763794,30.984525 19.178385,39.318533 4.592976,47.65254 0,52.916666 0,52.916666 Z"
id="path1"
sodipodi:nodetypes="ccczzzc" /></g></svg>
sodipodi:nodetypes="cccscccccccccccccccccccscsc" /></g><text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,-25.033368,-34.700052)"
id="text4"
style="font-weight:bold;font-size:64px;line-height:69.12px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold';letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect4);display:inline;fill:#346b23;fill-opacity:1;stroke-width:96;stroke-linecap:round"><tspan
x="317.6543"
y="289.52758"
id="tspan5"><tspan
style="font-weight:normal;font-family:FreeSerif;-inkscape-font-specification:FreeSerif"
id="tspan3">PerFin</tspan></tspan></text><text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,-24.420403,-19.32274)"
id="text5"
style="font-weight:bold;font-size:24px;line-height:25.92px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold';letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect5);display:inline;fill:#346b23;fill-opacity:1;stroke-width:96;stroke-linecap:round"><tspan
x="317.6543"
y="255.92758"
id="tspan7"><tspan
style="font-weight:normal;font-family:FreeSerif;-inkscape-font-specification:FreeSerif"
id="tspan6">Personal Finance</tspan></tspan></text></g></svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -7,6 +7,7 @@ import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.ImageCache;
import com.andrewlalis.perfin.view.SceneUtil;
import com.andrewlalis.perfin.view.StartupSplashScreen;
import com.andrewlalis.perfin.view.component.ScrollPaneRouterView;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
@ -38,7 +39,7 @@ public class PerfinApp extends Application {
* A router that controls which help page is being viewed in the side-pane.
* Certain user actions may cause this router to navigate to certain pages.
*/
public static final SceneRouter helpRouter = new SceneRouter(new AnchorPaneRouterView(true));
public static final SceneRouter helpRouter = new SceneRouter(new ScrollPaneRouterView());
public static void main(String[] args) {
launch(args);
@ -75,23 +76,24 @@ public class PerfinApp extends Application {
});
}
private static void mapResourceRoute(String route, String resource) {
router.map(route, PerfinApp.class.getResource(resource));
}
private static void defineRoutes(Consumer<String> msgConsumer) {
msgConsumer.accept("Initializing application views.");
Platform.runLater(() -> {
mapResourceRoute("accounts", "/accounts-view.fxml");
mapResourceRoute("account", "/account-view.fxml");
mapResourceRoute("edit-account", "/edit-account.fxml");
mapResourceRoute("transactions", "/transactions-view.fxml");
mapResourceRoute("create-transaction", "/create-transaction.fxml");
mapResourceRoute("create-balance-record", "/create-balance-record.fxml");
// App pages.
router.map("accounts", PerfinApp.class.getResource("/accounts-view.fxml"));
router.map("account", PerfinApp.class.getResource("/account-view.fxml"));
router.map("edit-account", PerfinApp.class.getResource("/edit-account.fxml"));
router.map("transactions", PerfinApp.class.getResource("/transactions-view.fxml"));
router.map("create-transaction", PerfinApp.class.getResource("/create-transaction.fxml"));
router.map("create-balance-record", PerfinApp.class.getResource("/create-balance-record.fxml"));
// Map help pages.
helpRouter.map("help-test", PerfinApp.class.getResource("/help-pages/help-test.fxml"));
// Help pages.
helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml"));
helpRouter.map("accounts", PerfinApp.class.getResource("/help-pages/accounts-view.fxml"));
helpRouter.map("adding-an-account", PerfinApp.class.getResource("/help-pages/adding-an-account.fxml"));
helpRouter.map("transactions", PerfinApp.class.getResource("/help-pages/transactions-view.fxml"));
helpRouter.map("adding-a-transaction", PerfinApp.class.getResource("/help-pages/adding-a-transaction.fxml"));
helpRouter.map("profiles", PerfinApp.class.getResource("/help-pages/profiles.fxml"));
});
}

View File

@ -3,12 +3,13 @@ package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView;
import com.andrewlalis.perfin.view.BindingUtil;
import com.andrewlalis.perfin.view.ProfilesStage;
import com.andrewlalis.perfin.view.component.ScrollPaneRouterView;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import static com.andrewlalis.perfin.PerfinApp.helpRouter;
import static com.andrewlalis.perfin.PerfinApp.router;
@ -19,7 +20,8 @@ public class MainViewController {
@FXML public Button showManualButton;
@FXML public Button hideManualButton;
@FXML public VBox manualVBox;
@FXML public BorderPane helpPane;
@FXML public Button helpBackButton;
@FXML public void initialize() {
AnchorPaneRouterView routerView = (AnchorPaneRouterView) router.getView();
@ -41,15 +43,26 @@ public class MainViewController {
router.navigate("accounts");
// Initialize the help manual components.
manualVBox.managedProperty().bind(manualVBox.visibleProperty());
manualVBox.setVisible(false);
helpPane.managedProperty().bind(helpPane.visibleProperty());
helpPane.setVisible(false);
showManualButton.managedProperty().bind(showManualButton.visibleProperty());
showManualButton.visibleProperty().bind(manualVBox.visibleProperty().not());
showManualButton.visibleProperty().bind(helpPane.visibleProperty().not());
hideManualButton.managedProperty().bind(hideManualButton.visibleProperty());
hideManualButton.visibleProperty().bind(manualVBox.visibleProperty());
hideManualButton.visibleProperty().bind(helpPane.visibleProperty());
AnchorPaneRouterView helpRouterView = (AnchorPaneRouterView) helpRouter.getView();
manualVBox.getChildren().add(helpRouterView.getAnchorPane());
helpBackButton.managedProperty().bind(helpBackButton.visibleProperty());
helpRouter.currentRouteProperty().addListener((observable, oldValue, newValue) -> {
helpBackButton.setVisible(helpRouter.getHistory().canGoBack());
});
helpBackButton.setOnAction(event -> helpRouter.navigateBack());
ScrollPaneRouterView helpRouterView = (ScrollPaneRouterView) helpRouter.getView();
ScrollPane helpRouterScrollPane = helpRouterView.getScrollPane();
helpRouterScrollPane.setMinWidth(200.0);
helpRouterScrollPane.setMaxWidth(400.0);
helpRouterScrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
helpRouterScrollPane.getStyleClass().addAll("padding-extra");
helpPane.setCenter(helpRouterScrollPane);
helpRouter.navigate("home");
}
@ -77,10 +90,25 @@ public class MainViewController {
}
@FXML public void showManual() {
manualVBox.setVisible(true);
helpPane.setVisible(true);
}
@FXML public void hideManual() {
manualVBox.setVisible(false);
helpPane.setVisible(false);
}
@FXML public void helpViewHome() {
helpRouter.getHistory().clear();
helpRouter.navigate("home");
}
@FXML public void helpViewAccounts() {
helpRouter.getHistory().clear();
helpRouter.navigate("accounts");
}
@FXML public void helpViewTransactions() {
helpRouter.getHistory().clear();
helpRouter.navigate("transactions");
}
}

View File

@ -5,8 +5,8 @@ import com.andrewlalis.perfin.data.ProfileLoadException;
import com.andrewlalis.perfin.data.util.FileUtil;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.ProfilesStage;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.BooleanProperty;
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
@ -16,6 +16,8 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
@ -24,22 +26,16 @@ import java.util.List;
import static com.andrewlalis.perfin.PerfinApp.router;
public class ProfilesViewController {
private static final Logger log = LoggerFactory.getLogger(ProfilesViewController.class);
@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());
var newProfileNameValid = new ValidationApplier<>(new PredicateValidator<String>()
.addPredicate(s -> s == null || s.isBlank() || Profile.validateName(s), "Profile name should consist of only lowercase numbers.")
).attachToTextField(newProfileNameField);
addProfileButton.disableProperty().bind(newProfileNameValid.not());
refreshAvailableProfiles();
@ -106,7 +102,7 @@ public class ProfilesViewController {
}
private boolean openProfile(String name, boolean showPopup) {
System.out.println("Opening profile: " + name);
log.info("Opening profile \"{}\".", name);
try {
Profile.load(name);
ProfilesStage.closeView();

View File

@ -62,10 +62,15 @@ public class StartupSplashScreen extends Stage implements Consumer<String> {
private void runTasks() {
Thread.ofVirtual().start(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (var task : tasks) {
try {
task.accept(this);
Thread.sleep(100);
Thread.sleep(500);
} catch (Exception e) {
accept("Startup failed: " + e.getMessage());
e.printStackTrace(System.err);
@ -80,7 +85,7 @@ public class StartupSplashScreen extends Stage implements Consumer<String> {
}
accept("Startup successful!");
try {
Thread.sleep(500);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

View File

@ -0,0 +1,23 @@
package com.andrewlalis.perfin.view.component;
import com.andrewlalis.javafx_scene_router.RouterView;
import javafx.scene.Parent;
import javafx.scene.control.ScrollPane;
public class ScrollPaneRouterView implements RouterView {
private final ScrollPane scrollPane = new ScrollPane();
public ScrollPaneRouterView() {
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
}
@Override
public void showRouteNode(Parent node) {
scrollPane.setContent(node);
}
public ScrollPane getScrollPane() {
return scrollPane;
}
}

View File

@ -0,0 +1,181 @@
package com.andrewlalis.perfin.view.component;
import com.andrewlalis.perfin.PerfinApp;
import javafx.beans.DefaultProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.geometry.Insets;
import javafx.scene.AccessibleAttribute;
import javafx.scene.control.Hyperlink;
import javafx.scene.layout.Border;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import java.util.ArrayList;
import java.util.List;
import static com.andrewlalis.perfin.PerfinApp.helpRouter;
/**
* A component that renders markdown-ish text as a series of TextFlow elements,
* styled according to some basic styles.
*/
@DefaultProperty("text")
public class StyledText extends VBox {
private StringProperty text;
private boolean initialized = false;
public final void setText(String value) {
if (value == null) value = "";
textProperty().set(value);
}
public final String getText() {
return text == null ? "" : text.get();
}
public final StringProperty textProperty() {
if (text == null) {
text = new StringPropertyBase("") {
@Override public Object getBean() { return StyledText.this; }
@Override public String getName() { return "text"; }
@Override public void invalidated() {
notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT);
}
};
}
return text;
}
@Override
protected void layoutChildren() {
if (!initialized) {
String s = getText();
getChildren().clear();
getChildren().addAll(renderText(s));
getStyleClass().add("spacing-extra");
initialized = true;
}
super.layoutChildren();
}
private List<TextFlow> renderText(String text) {
return new TextFlowBuilder().build(text);
}
private static class TextFlowBuilder {
private final List<TextFlow> flows = new ArrayList<>();
private int idx = 0;
private final StringBuilder currentRun = new StringBuilder();
private TextFlow currentParagraph;
public List<TextFlow> build(String text) {
flows.clear();
idx = 0;
currentRun.setLength(0);
currentParagraph = new TextFlow();
while (idx < text.length()) {
if (text.startsWith("**", idx)) {
parseStyledText(text, "**", "bold-text");
} else if (text.startsWith("*", idx)) {
parseStyledText(text, "*", "italic-text");
} else if (text.startsWith("`", idx)) {
parseStyledText(text, "`", "mono-font");
} else if (text.startsWith(" -- ", idx)) {
parsePageBreak(text);
} else if (text.startsWith("[", idx)) {
parseLink(text);
} else if (text.startsWith("#", idx) && (idx == 0 || (idx > 0 && text.charAt(idx - 1) == ' '))) {
parseHeader(text);
} else {
currentRun.append(text.charAt(idx));
idx++;
}
}
appendTextIfPresent();
appendParagraphIfPresent();
return flows;
}
private void parsePageBreak(String text) {
appendTextIfPresent();
appendParagraphIfPresent();
while (text.charAt(idx) == ' ') idx++;
while (text.charAt(idx) == '-') idx++;
while (text.charAt(idx) == ' ') idx++;
}
private void parseStyledText(String text, String marker, String styleClass) {
appendTextIfPresent();
int endIdx = text.indexOf(marker, idx + marker.length());
Text textItem = new Text(text.substring(idx + marker.length(), endIdx));
textItem.getStyleClass().add(styleClass);
currentParagraph.getChildren().add(textItem);
idx = endIdx + marker.length();
}
private void parseLink(String text) {
appendTextIfPresent();
int labelEndIdx = text.indexOf(']', idx);
String label = text.substring(idx + 1, labelEndIdx);
idx = labelEndIdx + 1;
final String link;
if (text.charAt(labelEndIdx + 1) == '(') {
int linkEndIdx = text.indexOf(')', labelEndIdx + 2);
link = text.substring(labelEndIdx + 2, linkEndIdx);
idx = linkEndIdx + 1;
} else {
link = null;
}
Hyperlink hyperlink = new Hyperlink(label);
if (link != null) {
if (link.startsWith("http")) {
hyperlink.setOnAction(event -> PerfinApp.instance.getHostServices().showDocument(link));
} else if (link.startsWith("help:")) {
hyperlink.setOnAction(event -> helpRouter.navigate(link.substring(5).strip()));
}
}
hyperlink.setBorder(Border.EMPTY);
hyperlink.setPadding(new Insets(0, 0, 0, 0));
currentParagraph.getChildren().add(hyperlink);
}
private void parseHeader(String text) {
appendTextIfPresent();
appendParagraphIfPresent();
int size = 0;
while (text.charAt(idx) == '#') {
idx++;
size++;
}
int endIdx = text.indexOf("#".repeat(size), idx);
Text header = new Text(text.substring(idx, endIdx).strip());
idx = endIdx + size;
while (text.charAt(idx) == ' ') idx++;
String styleClass = switch(size) {
case 1 -> "large-font";
case 2 -> "largest-font";
default -> "largest-font";
};
header.getStyleClass().addAll(styleClass, "bold-text");
currentParagraph.getChildren().add(header);
appendParagraphIfPresent();
}
private void appendTextIfPresent() {
if (!currentRun.isEmpty()) {
currentParagraph.getChildren().add(new Text(currentRun.toString()));
currentRun.setLength(0);
}
}
private void appendParagraphIfPresent() {
if (!currentParagraph.getChildren().isEmpty()) {
flows.add(currentParagraph);
currentParagraph = new TextFlow();
}
}
}
}

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.andrewlalis.perfin.view.component.StyledText?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
styleClass="std-spacing"
>
<StyledText>
## The Accounts View ##
In the *Accounts view*, you'll see an overview of all the active
accounts in your *Perfin* profile.
--
To add a new account, simply click on **Add an Account** from the menu
at the top. [Read about how to add an account here.](help:adding-an-account)
--
Additionally, the top menu also shows an overview of the derived total
balance for each currency you own. This adds up the balances of all
accounts of like currency, and gives you a total of each.
--
Accounts are, by default, ordered according to the most recent activity.
That means that your most active accounts will appear *first* in this
page, and your least active accounts will appear *last*.
# Account Tiles #
Each account's tile displays some basic information about the account
up-front, like its name, number, balance, and type. Click on a tile to
navigate to that account's page for more detailed information, and to
make changes to the account.
--
The *current balance* shown in each account tile is derived from the
account's last-known **balance record**, and all transactions that have
happened between then and now.
</StyledText>
</VBox>

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import com.andrewlalis.perfin.view.component.StyledText?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
>
<StyledText>
## Adding a Transaction ##
When you're adding a new transaction to your *Perfin* profile, there are
some details that you should be aware of.
# Timestamp #
The timestamp is the date and time at which the transaction took place,
in your local time zone. Generally, it's encouraged to set this as the
real timestamp at which the transaction took place (the time shown on a
receipt or invoice, for example), rather than the timestamp provided by
your financial institution once the payment has been processed.
--
It's formatted as `yyyy-mm-dd HH:MM:SS`, so for example, November 14th,
2015 at 4:15pm would be written as `2015-11-14 16:15:00`. You can omit
the seconds if you like, however.
--
Also note that you may not enter timestamps from the future; it just
doesn't make sense to do so.
# Amount #
The total amount of the transaction, as a positive decimal value. This
is the final amount which you've paid or received, including any tax,
tips, or transaction fees.
# Currency #
The currency of the transaction. This should be the same currency as the
account(s) that the transaction is linked to.
# Linked Debit and Credit Accounts #
Every transaction, for it to mean something, needs to be linked to one
(or sometimes two) of your accounts. A transaction's impact on an
account depends on whether the transaction is being treated as a **Debit**
or a **Credit** on the account.
--
The account linked as **Debit** is the one whose assets will
**increase** as a result of the transaction. Some common examples of
transactions with debit-linked accounts include deposits to checking
or savings accounts, or refunds to credit cards.
--
The account linked as **Credit** is the one whose assets will
**decrease** as a result of the transaction. Some common examples of
transactions with credit-linked accounts include payments or purchases
with a checking or savings account, or a credit card.
--
In short, if your account is *gaining money*, link it under **debit**.
If your account is *losing money*, link it under **credit**.
--
For transfers between two accounts that are both tracked in *Perfin*,
the *sending* account should be linked under **credit**, and the
*receiving* account under **debit**.
# Attachments #
Often, you'll have a receipt, invoice, bank statement, or some other
document which acts as proof of a transaction. You can attach files to
a transaction to save those files with it, as a reference. The files
will be copied to your *Perfin* profile.
</StyledText>
</VBox>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import com.andrewlalis.perfin.view.component.StyledText?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
>
<StyledText>
## Adding an Account ##
When adding an account, you'll need to provide some basic information
about the account so that Perfin can integrate it with its automatic
balance calculations and other functions.
# Name #
The name of the account is just a bit of text that you can use to
identify the account from the rest of yours. Make sure it's unique, and
to-the-point, since the account's name is used elsewhere in the app to
refer to the account.
# Number #
The number of the account is the unique account number provided by your
financial institution. For checking and savings accounts, it'll be the
account number provided by your bank, and for credit cards, it'll be the
credit card's number.
# Currency #
The currency is pretty self-explanatory; simply select the currency that
your account uses. Perfin uses 3-character *currency codes* quite often,
and you can read about them [here](https://en.wikipedia.org/wiki/ISO_4217#List_of_currency_codes).
# Account Type #
The account's type determines how certain balance calculations and
properties of the account work. For instance, a **Credit Card**
account's balance is interpreted to be the amount you owe to the card's
provider (the amount you need to pay off), while a **Checking**
account's balance is naturally the amount of money in your account.
# Initial Balance #
In order to accurately compute your account's balance (without needing
to check back every hour with your financial institution), Perfin needs
to know what your account's starting balance is. This way, it can use
that balance, combined with any transactions you add, to tell you your
balance at any moment in time.
</StyledText>
</VBox>

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.text.TextFlow?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
>
<TextFlow>
<Text>
This is a testing help page, which is meant to be shown only for
testing the help system and navigation.
</Text>
</TextFlow>
</VBox>

View File

@ -1,29 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import com.andrewlalis.perfin.view.component.StyledText?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.text.TextFlow?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
styleClass="std-spacing"
>
<Label styleClass="large-font,bold-text" text="Perfin Help Pages"/>
<TextFlow>
<Text>
This is the homepage for Perfin's help pages: a manual for how to
use Perfin safely and effectively to manage your personal finances.
</Text>
</TextFlow>
<TextFlow>
<Text>
Search for a topic below, or click on the help icon beside some
component to navigate to its page.
</Text>
</TextFlow>
<StyledText>
## Homepage ##
This is the homepage for Perfin's help pages: a manual for how to use
Perfin safely and effectively to manage your personal finances.
--
Search for a topic below, or follow one of the index links.
</StyledText>
<HBox>
<Label text="Search!"/>
<TextField/>
<TextField disable="true" text="Searching is WIP" HBox.hgrow="ALWAYS"/>
</HBox>
<StyledText>
# Help Pages Index #
The following is a list of all help pages.
--
[Accounts View](help:accounts)
--
[Adding an Account](help:adding-an-account)
--
[Transactions View](help:transactions)
--
[Adding a Transaction](help:adding-a-transaction)
--
[Profiles](help:profiles)
</StyledText>
</VBox>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import com.andrewlalis.perfin.view.component.StyledText?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
>
<StyledText>
## Profiles ##
In *Perfin*, all the accounts, transactions, attachments, bank
statements, and other financial data, are stored in a *profile*. You can
think of it as a save file. It's a single folder that encapsulates all
your data.
--
Perfin uses profiles to make it easy to work with multiple financial
portfolios, export and import bulk data, and to let *you*, the user,
access your data directly, as opposed to hiding it in some proprietary
file format.
--
A running Perfin program always has one profile open at a time. You can
change the active profile by clicking **Profiles** from the app's top
menu, and then clicking **Open** next to the profile you'd like to use.
--
By default, when you first start Perfin, a profile named *default* is
created for you to use. Most of the time, you won't need to worry about
using other profiles, but if you do, now you know how.
</StyledText>
</VBox>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.andrewlalis.perfin.view.component.StyledText?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
styleClass="std-spacing"
>
<StyledText>
## The Transactions View ##
In the *Transactions view*, you're shown a list of all transactions that
have been added to your *Perfin* profile, along with some controls for
navigating this list.
--
The transactions are ordered, by default, according to their timestamp,
with recent transactions appearing before older ones.
--
Simply click on a transaction to view its details or make changes.
--
To add a new transaction, click **Add Transaction** in the top menu. You
can read more about adding a transaction [here](help:adding-a-transaction).
</StyledText>
</VBox>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -5,30 +5,52 @@
<BorderPane
xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:id="mainContainer"
fx:controller="com.andrewlalis.perfin.control.MainViewController"
>
<top>
<VBox>
<HBox styleClass="std-padding,std-spacing">
<Button text="Back" onAction="#goBack"/>
<Button text="Forward" onAction="#goForward"/>
<Button text="Accounts" onAction="#goToAccounts"/>
<Button text="Transactions" onAction="#goToTransactions"/>
<Button text="Profiles" onAction="#viewProfiles"/>
<center>
<BorderPane fx:id="mainContainer">
<!-- Top bar for the app -->
<top>
<VBox>
<HBox styleClass="std-padding,std-spacing">
<Button text="Back" onAction="#goBack"/>
<Button text="Forward" onAction="#goForward"/>
<Button text="Accounts" onAction="#goToAccounts"/>
<Button text="Transactions" onAction="#goToTransactions"/>
<Button text="Profiles" onAction="#viewProfiles"/>
<Button text="View Manual" fx:id="showManualButton" onAction="#showManual"/>
<Button text="Hide Manual" fx:id="hideManualButton" onAction="#hideManual"/>
</HBox>
<HBox fx:id="breadcrumbHBox" styleClass="std-spacing,small-font"/>
</VBox>
</top>
<bottom>
<HBox styleClass="std-padding,std-spacing">
<Label text="Perfin Version 1.2.0"/>
</HBox>
</bottom>
<Button text="View Help" fx:id="showManualButton" onAction="#showManual"/>
<Button text="Hide Help" fx:id="hideManualButton" onAction="#hideManual"/>
</HBox>
<HBox fx:id="breadcrumbHBox" styleClass="std-spacing,small-font"/>
</VBox>
</top>
<!-- App footer -->
<bottom>
<HBox styleClass="std-padding,std-spacing">
<Label text="Perfin Version 1.2.0"/>
<AnchorPane>
<Label text="© 2024 Andrew Lalis" styleClass="small-font,secondary-color-text-fill" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0"/>
</AnchorPane>
</HBox>
</bottom>
</BorderPane>
</center>
<!-- Right-side panel to show help info, at the top level of the whole app. -->
<right>
<VBox fx:id="manualVBox" style="-fx-min-width: 400px;" styleClass="padding-extra-1"/>
<BorderPane fx:id="helpPane">
<top>
<VBox styleClass="padding-extra-1">
<Label text="Perfin Help" styleClass="largest-font,bold-text"/>
<HBox styleClass="std-spacing">
<Hyperlink onAction="#helpViewHome">Home</Hyperlink>
<Hyperlink onAction="#helpViewAccounts">Accounts</Hyperlink>
<Hyperlink onAction="#helpViewTransactions">Transactions</Hyperlink>
</HBox>
<Button fx:id="helpBackButton" text="Back"/>
</VBox>
</top>
</BorderPane>
</right>
</BorderPane>

View File

@ -39,19 +39,13 @@
<bottom>
<BorderPane>
<left>
<VBox styleClass="std-padding">
<Label text="Add New Profile"/>
</VBox>
<AnchorPane styleClass="std-padding">
<Label text="Add New Profile" styleClass="bold-text" AnchorPane.leftAnchor="0" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0"/>
</AnchorPane>
</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>