Added icon, image caching, and improved history item tiles.

This commit is contained in:
Andrew Lalis 2023-12-31 09:37:16 -05:00
parent 651396739f
commit e42f9507db
13 changed files with 248 additions and 84 deletions

78
design/perfin-logo.svg Normal file
View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64"
height="64"
viewBox="0 0 16.933333 16.933333"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="perfin-logo.svg"
inkscape:export-filename="../src/main/resources/images/perfin-logo_64.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
inkscape:zoom="5.6568543"
inkscape:cx="3.2703689"
inkscape:cy="25.102291"
inkscape:window-width="1920"
inkscape:window-height="1025"
inkscape:window-x="1080"
inkscape:window-y="470"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<rect
x="23.509132"
y="17.511823"
width="25.696689"
height="28.301114"
id="rect2" />
<rect
x="23.509132"
y="17.511823"
width="25.696689"
height="28.301114"
id="rect3" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#346b23;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round"
id="rect1"
width="16.933332"
height="16.933332"
x="-1.7763568e-15"
y="1.7763568e-15" />
<path
style="fill:#ca9c00;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round"
d="M -1.3919068e-7,16.933333 16.933333,3.7161866e-7 V 16.933333 H -1.3919068e-7"
id="path2" />
<path
id="path1"
style="fill:#346b23;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round"
d="m 13.000241,3.9330926 -1.304313,1.304313 c 0.0076,0.032651 0.01631,0.063696 0.02325,0.097152 L 13.398662,5.0162308 C 13.290974,4.6118006 13.157768,4.2511887 13.000237,3.9330929 Z M 9.537919,7.3954134 7.984009,8.9493237 V 13.384195 C 7.108943,13.339155 6.414126,13.117013 5.899382,12.718086 5.591151,12.475354 5.346639,12.158421 5.162993,11.77034 l -1.204061,1.204061 c 0.792283,1.097222 2.132722,1.678716 4.025077,1.741495 v 1.553911 h 1.235584 v -1.553911 c 1.441285,-0.05791 2.548094,-0.389429 3.320211,-0.994254 0.772117,-0.611259 1.158069,-1.437594 1.158069,-2.479952 0,-0.514745 -0.07422,-0.96528 -0.222209,-1.3513385 C 13.327675,9.4978588 13.10605,9.1533177 12.810072,8.8573396 12.514093,8.5613615 12.117989,8.2978085 11.622547,8.0661735 11.178148,7.8584026 10.474116,7.6342452 9.537919,7.3954134 Z M 9.219593,8.8573396 c 0.8815,0.2252008 1.482826,0.4314552 1.804541,0.6180501 0.321715,0.1801606 0.569381,0.4085543 0.743107,0.6852293 0.180161,0.276675 0.270268,0.627468 0.270268,1.052132 0,0.649865 -0.23803,1.164468 -0.714168,1.544092 -0.476139,0.37319 -1.177208,0.588563 -2.103748,0.646472 z" />
<path
id="text3"
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);fill:#ca9c00;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round"
d="M 25.689419 18.184344 L 25.689419 18.692161 C 25.145149 18.710391 24.729758 18.828758 24.4433 19.047508 C 24.159446 19.263654 24.017678 19.565737 24.017678 19.953758 C 24.017678 20.198549 24.064478 20.409564 24.158228 20.586647 C 24.254582 20.763731 24.404358 20.916037 24.607483 21.043641 C 24.813212 21.168641 25.134795 21.284479 25.572295 21.39125 L 25.689419 21.422622 L 25.689419 21.537864 L 26.318335 20.908948 C 26.274814 20.897846 26.235644 20.886962 26.189498 20.875694 L 26.189498 19.211691 C 26.473352 19.229921 26.698597 19.309289 26.865264 19.449914 C 27.023755 19.583641 27.132164 19.779351 27.191748 20.035536 L 27.719644 19.50764 C 27.611975 19.290229 27.475742 19.122396 27.310545 19.004632 C 27.047525 18.814528 26.673873 18.710391 26.189498 18.692161 L 26.189498 18.184344 L 25.689419 18.184344 z M 25.689419 19.203744 L 25.689419 20.817131 C 25.379523 20.736402 25.165978 20.663464 25.048791 20.59836 C 24.931603 20.530652 24.841696 20.447383 24.779196 20.348425 C 24.7193 20.246863 24.68947 20.121855 24.68947 19.973418 C 24.68947 19.736439 24.774108 19.552723 24.943379 19.422515 C 25.112649 19.289703 25.361294 19.216765 25.689419 19.203744 z M 24.431587 22.324898 L 23.767534 22.469421 C 23.822782 22.741711 23.9209 22.973823 24.060345 23.166938 L 24.547666 22.679617 C 24.497867 22.574382 24.458752 22.45654 24.431587 22.324898 z "
transform="matrix(2.4707763,0,0,2.4707763,-55.488799,-44.26592)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -3,6 +3,7 @@ package com.andrewlalis.perfin;
import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView; import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView;
import com.andrewlalis.javafx_scene_router.SceneRouter; import com.andrewlalis.javafx_scene_router.SceneRouter;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.ImageCache;
import com.andrewlalis.perfin.view.SceneUtil; import com.andrewlalis.perfin.view.SceneUtil;
import com.andrewlalis.perfin.view.StartupSplashScreen; import com.andrewlalis.perfin.view.StartupSplashScreen;
import javafx.application.Application; import javafx.application.Application;
@ -20,6 +21,7 @@ import java.util.function.Consumer;
*/ */
public class PerfinApp extends Application { public class PerfinApp extends Application {
public static final Path APP_DIR = Path.of(System.getProperty("user.home", "."), ".perfin"); public static final Path APP_DIR = Path.of(System.getProperty("user.home", "."), ".perfin");
public static PerfinApp instance;
/** /**
* The router that's used for navigating between different "pages" in the application. * The router that's used for navigating between different "pages" in the application.
@ -32,6 +34,7 @@ public class PerfinApp extends Application {
@Override @Override
public void start(Stage stage) { public void start(Stage stage) {
instance = this;
var splashScreen = new StartupSplashScreen(List.of( var splashScreen = new StartupSplashScreen(List.of(
PerfinApp::defineRoutes, PerfinApp::defineRoutes,
PerfinApp::initAppDir, PerfinApp::initAppDir,
@ -51,6 +54,7 @@ public class PerfinApp extends Application {
Scene mainViewScene = SceneUtil.load("/main-view.fxml"); Scene mainViewScene = SceneUtil.load("/main-view.fxml");
stage.setScene(mainViewScene); stage.setScene(mainViewScene);
stage.setTitle("Perfin"); stage.setTitle("Perfin");
stage.getIcons().add(ImageCache.getLogo64());
}); });
} }

View File

@ -1,20 +1,16 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DateUtil; import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.model.history.AccountHistoryItem; import com.andrewlalis.perfin.model.history.AccountHistoryItem;
import com.andrewlalis.perfin.view.component.AccountHistoryItemTile;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@ -39,7 +35,7 @@ public class AccountViewController implements RouteSelectionListener {
@Override @Override
public void onRouteSelected(Object context) { public void onRouteSelected(Object context) {
account = (Account) context; account = (Account) context;
titleLabel.setText("Account: " + account.getAccountNumber()); titleLabel.setText("Account #" + account.getId());
accountNameField.setText(account.getName()); accountNameField.setText(account.getName());
accountNumberField.setText(account.getAccountNumber()); accountNumberField.setText(account.getAccountNumber());
@ -105,42 +101,13 @@ public class AccountViewController implements RouteSelectionListener {
} else { } else {
loadHistoryFrom = historyItems.getLast().getTimestamp(); loadHistoryFrom = historyItems.getLast().getTimestamp();
} }
List<Node> nodes = historyItems.stream().map(item -> visualizeHistoryItem(item, historyRepo)).toList(); List<? extends Node> nodes = historyItems.stream()
.map(item -> new AccountHistoryItemTile(item, historyRepo))
.toList();
Platform.runLater(() -> historyItemsVBox.getChildren().addAll(nodes)); Platform.runLater(() -> historyItemsVBox.getChildren().addAll(nodes));
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
} }
private Node visualizeHistoryItem(AccountHistoryItem item, AccountHistoryItemRepository repo) {
BorderPane containerPane = new BorderPane();
containerPane.setStyle("""
-fx-border-color: lightgray;
-fx-border-radius: 5px;
-fx-padding: 5px;
""");
Label timestampLabel = new Label(item.getTimestamp().format(DateUtil.DEFAULT_DATETIME_FORMAT));
timestampLabel.setStyle("-fx-font-size: small;");
containerPane.setTop(timestampLabel);
containerPane.setCenter(switch (item.getType()) {
case TEXT -> {
var text = repo.getTextItem(item.getId());
yield new TextFlow(new Text(text));
}
case ACCOUNT_ENTRY -> {
var entry = repo.getAccountEntryItem(item.getId());
Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency()));
TextFlow text = new TextFlow(new Text("Entry added with value of "), amountText);
yield text;
}
case BALANCE_RECORD -> {
var balanceRecord = repo.getBalanceRecordItem(item.getId());
Text amountText = new Text(CurrencyUtil.formatMoney(balanceRecord.getBalance(), balanceRecord.getCurrency()));
TextFlow text = new TextFlow(new Text("Balance record added with value of "), amountText);
yield text;
}
});
return containerPane;
}
} }

View File

@ -1,5 +1,6 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.perfin.PerfinApp;
import com.andrewlalis.perfin.data.util.FileUtil; import com.andrewlalis.perfin.data.util.FileUtil;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.ProfilesStage; import com.andrewlalis.perfin.view.ProfilesStage;
@ -95,6 +96,11 @@ public class ProfilesViewController {
openButton.setOnAction(event -> openProfile(profileName, false)); openButton.setOnAction(event -> openProfile(profileName, false));
openButton.setDisable(isCurrent); openButton.setDisable(isCurrent);
buttonBox.getChildren().add(openButton); buttonBox.getChildren().add(openButton);
Button viewFilesButton = new Button("View Files");
viewFilesButton.setOnAction(event -> {
PerfinApp.instance.getHostServices().showDocument(Profile.getDir(profileName).toUri().toString());
});
buttonBox.getChildren().add(viewFilesButton);
Button deleteButton = new Button("Delete"); Button deleteButton = new Button("Delete");
deleteButton.setOnAction(event -> deleteProfile(profileName)); deleteButton.setOnAction(event -> deleteProfile(profileName));
buttonBox.getChildren().add(deleteButton); buttonBox.getChildren().add(deleteButton);

View File

@ -27,6 +27,8 @@ import static com.andrewlalis.perfin.PerfinApp.router;
public class TransactionViewController { public class TransactionViewController {
private Transaction transaction; private Transaction transaction;
@FXML public Label titleLabel;
@FXML public Label amountLabel; @FXML public Label amountLabel;
@FXML public Label timestampLabel; @FXML public Label timestampLabel;
@FXML public Label descriptionLabel; @FXML public Label descriptionLabel;
@ -41,6 +43,7 @@ public class TransactionViewController {
public void setTransaction(Transaction transaction) { public void setTransaction(Transaction transaction) {
this.transaction = transaction; this.transaction = transaction;
if (transaction == null) return; if (transaction == null) return;
titleLabel.setText("Transaction #" + transaction.getId());
amountLabel.setText(CurrencyUtil.formatMoney(transaction.getAmount(), transaction.getCurrency())); amountLabel.setText(CurrencyUtil.formatMoney(transaction.getAmount(), transaction.getCurrency()));
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp())); timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp()));
descriptionLabel.setText(transaction.getDescription()); descriptionLabel.setText(transaction.getDescription());

View File

@ -0,0 +1,40 @@
package com.andrewlalis.perfin.view;
import javafx.scene.image.Image;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ImageCache {
public static final ImageCache instance = new ImageCache();
private final Map<String, Image> images = new ConcurrentHashMap<>();
public Image get(String resource, double width, double height, boolean preserveRatio, boolean smooth) {
final String cacheKey = getCacheKey(resource, width, height, preserveRatio, smooth);
Image stored = images.get(cacheKey);
if (stored != null) return stored;
try (var in = ImageCache.class.getResourceAsStream(resource)) {
if (in == null) throw new IOException("Could not load resource " + resource);
Image img = new Image(in, width, height, preserveRatio, smooth);
images.put(cacheKey, img);
return img;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private String getCacheKey(String resource, double width, double height, boolean preserveRatio, boolean smooth) {
return resource + "_" +
"W" + width + "_" +
"H" + height + "_" +
"PR-" + preserveRatio + "_" +
"S-" + smooth;
}
public static Image getLogo64() {
return instance.get("/images/perfin-logo_64.png", 64, 64, true, true);
}
}

View File

@ -26,6 +26,7 @@ public class StartupSplashScreen extends Stage implements Consumer<String> {
setTitle("Starting Perfin..."); setTitle("Starting Perfin...");
setResizable(false); setResizable(false);
initStyle(StageStyle.UNDECORATED); initStyle(StageStyle.UNDECORATED);
getIcons().add(ImageCache.getLogo64());
setScene(buildScene()); setScene(buildScene());
setOnShowing(event -> runTasks()); setOnShowing(event -> runTasks());

View File

@ -0,0 +1,57 @@
package com.andrewlalis.perfin.view.component;
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.model.AccountEntry;
import com.andrewlalis.perfin.model.BalanceRecord;
import com.andrewlalis.perfin.model.history.AccountHistoryItem;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
/**
* A tile that shows a brief bit of information about an account history item.
*/
public class AccountHistoryItemTile extends BorderPane {
public AccountHistoryItemTile(AccountHistoryItem item, AccountHistoryItemRepository repo) {
setStyle("""
-fx-border-color: lightgray;
-fx-border-radius: 5px;
-fx-padding: 5px;
""");
Label timestampLabel = new Label(DateUtil.formatUTCAsLocalWithZone(item.getTimestamp()));
timestampLabel.setStyle("-fx-font-size: small;");
setTop(timestampLabel);
setCenter(switch (item.getType()) {
case TEXT -> buildTextItem(repo.getTextItem(item.getId()));
case ACCOUNT_ENTRY -> buildAccountEntryItem(repo.getAccountEntryItem(item.getId()));
case BALANCE_RECORD -> buildBalanceRecordItem(repo.getBalanceRecordItem(item.getId()));
});
}
private Node buildTextItem(String text) {
return new TextFlow(new Text(text));
}
private Node buildAccountEntryItem(AccountEntry entry) {
Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency()));
Hyperlink transactionLink = new Hyperlink("Transaction #" + entry.getTransactionId());
return new TextFlow(
new Text("Entry added with value of "),
amountText,
new Text(", linked with "),
transactionLink,
new Text(".")
);
}
private Node buildBalanceRecordItem(BalanceRecord balanceRecord) {
Text amountText = new Text(CurrencyUtil.formatMoney(balanceRecord.getBalance(), balanceRecord.getCurrency()));
return new TextFlow(new Text("Balance record added with value of "), amountText);
}
}

View File

@ -2,6 +2,7 @@ package com.andrewlalis.perfin.view.component;
import com.andrewlalis.perfin.model.Attachment; import com.andrewlalis.perfin.model.Attachment;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.view.ImageCache;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
@ -43,13 +44,7 @@ public class AttachmentPreview extends BorderPane {
} }
} }
if (showDocIcon) { if (showDocIcon) {
try (var in = AttachmentPreview.class.getResourceAsStream("/images/doc-icon.png")) { contentContainer.setCenter(new ImageView(ImageCache.instance.get("/images/doc-icon.png", 64, 64, true, true)));
if (in == null) throw new NullPointerException("Missing /images/doc-icon.png resource.");
Image img = new Image(in, IMAGE_SIZE, IMAGE_SIZE, true, true);
contentContainer.setCenter(new ImageView(img));
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
BorderPane hoverIndicatorPane = new BorderPane(); BorderPane hoverIndicatorPane = new BorderPane();

View File

@ -7,10 +7,10 @@ module com.andrewlalis.perfin {
requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.databind;
requires java.sql;
requires com.github.f4b6a3.ulid; requires com.github.f4b6a3.ulid;
requires java.sql;
exports com.andrewlalis.perfin to javafx.graphics; exports com.andrewlalis.perfin to javafx.graphics;
exports com.andrewlalis.perfin.view to javafx.graphics; exports com.andrewlalis.perfin.view to javafx.graphics;
exports com.andrewlalis.perfin.model to javafx.graphics; exports com.andrewlalis.perfin.model to javafx.graphics;

View File

@ -13,11 +13,15 @@
<Label fx:id="titleLabel" styleClass="large-text,bold-text"/> <Label fx:id="titleLabel" styleClass="large-text,bold-text"/>
</HBox> </HBox>
</top> </top>
<center>
<VBox>
<!-- Main account properties and actions -->
<BorderPane>
<center> <center>
<VBox styleClass="std-padding,std-spacing"> <VBox styleClass="std-padding,std-spacing">
<HBox> <HBox>
<!-- Main account properties. --> <!-- Main account properties. -->
<VBox HBox.hgrow="SOMETIMES" style="-fx-border-color: blue"> <VBox HBox.hgrow="SOMETIMES">
<VBox styleClass="account-property-box"> <VBox styleClass="account-property-box">
<Label text="Name"/> <Label text="Name"/>
<TextField fx:id="accountNameField" editable="false"/> <TextField fx:id="accountNameField" editable="false"/>
@ -39,12 +43,24 @@
<TextField fx:id="accountBalanceField" editable="false"/> <TextField fx:id="accountBalanceField" editable="false"/>
</VBox> </VBox>
</VBox> </VBox>
<VBox HBox.hgrow="SOMETIMES" style="-fx-border-color: red"> <VBox HBox.hgrow="SOMETIMES">
<Label text="Panel 2"/> <Label text="Panel 2"/>
</VBox> </VBox>
</HBox> </HBox>
<Separator/> </VBox>
<VBox> </center>
<right>
<VBox styleClass="std-padding,std-spacing">
<Label text="Actions" styleClass="bold-text"/>
<Button text="Edit" onAction="#goToEditPage"/>
<Button text="Archive" onAction="#archiveAccount"/>
<Button text="Delete" onAction="#deleteAccount"/>
</VBox>
</right>
</BorderPane>
<!-- Account history -->
<VBox VBox.vgrow="ALWAYS">
<Label text="History" styleClass="bold-text"/> <Label text="History" styleClass="bold-text"/>
<VBox> <VBox>
<ScrollPane fitToHeight="true" fitToWidth="true"> <ScrollPane fitToHeight="true" fitToWidth="true">
@ -63,12 +79,4 @@
</VBox> </VBox>
</VBox> </VBox>
</center> </center>
<right>
<VBox styleClass="std-padding,std-spacing">
<Label text="Actions" styleClass="bold-text"/>
<Button text="Edit" onAction="#goToEditPage"/>
<Button text="Archive" onAction="#archiveAccount"/>
<Button text="Delete" onAction="#deleteAccount"/>
</VBox>
</right>
</BorderPane> </BorderPane>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -8,6 +8,11 @@
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.TransactionViewController" fx:controller="com.andrewlalis.perfin.control.TransactionViewController"
> >
<top>
<HBox styleClass="std-padding,std-spacing">
<Label fx:id="titleLabel" styleClass="large-text,bold-text"/>
</HBox>
</top>
<center> <center>
<ScrollPane fitToHeight="true" fitToWidth="true"> <ScrollPane fitToHeight="true" fitToWidth="true">
<VBox styleClass="std-padding,std-spacing"> <VBox styleClass="std-padding,std-spacing">