diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3619b0e..03e76c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,11 @@
### Features
+- (Base) New `DeckPane` component with swipe and slide transition support.
- (CSS) 🚀 New MacOS-like Cupertino theme in light and dark variants.
- (CSS) 🚀 New [Dracula](https://ui.draculatheme.com/) theme.
-- (CSS) 🚀 New `TabPane` style. There are three styles supported: default, floating and classic (new one).
+- (CSS) New `TabPane` style. There are three styles supported: default, floating and classic (new one).
+- (Sampler) 🚀 SceneBuilder integration. AtlantaFX themes can be installed (or uninstalled) to SceneBuilder directly from the Sampler app.
### Improvements
diff --git a/base/src/main/java/module-info.java b/base/src/main/java/module-info.java
index d56c55c..43d017d 100755
--- a/base/src/main/java/module-info.java
+++ b/base/src/main/java/module-info.java
@@ -6,6 +6,7 @@ module atlantafx.base {
requires static org.jetbrains.annotations;
exports atlantafx.base.controls;
+ exports atlantafx.base.layout;
exports atlantafx.base.theme;
exports atlantafx.base.util;
diff --git a/docs/docs/assets/images/scene-builder-integration.png b/docs/docs/assets/images/scene-builder-integration.png
new file mode 100644
index 0000000..12217cb
Binary files /dev/null and b/docs/docs/assets/images/scene-builder-integration.png differ
diff --git a/docs/docs/fxml.md b/docs/docs/fxml.md
index af06c45..ec51b8e 100644
--- a/docs/docs/fxml.md
+++ b/docs/docs/fxml.md
@@ -8,23 +8,28 @@ See the corresponding [issue](https://github.com/mkpaz/atlantafx/issues/27).
While SceneBuilder does not support adding custom themes, it is possible to overwrite looked-up CSS paths to make the existing buttons load custom CSS files.
-In order to use AtlantaFX in SceneBuilder you need to:
+**(not yet released)**
-* Run `mvn package -pl styles` to generate theme CSS files with the correct path names.
-* Copy `styles/target/AtlantaFX-${version}-scenebuilder.zip` to the SceneBuilder `$APPDIR` (e.g. `%HOMEPATH%/Local/SceneBuilder/app/` on Windows) or another directory of your choice.
-* Open `SceneBuilder.cfg` in the SceneBuilder app directory and add the zip file to the beginning of the `app.classpath` variable, e.g.:
+![Project structure](assets/images/scene-builder-integration.png)
+
+### Manual
+
+* Run `mvn package -pl styles` to generate theme package. You can also download it on the [Releases](https://github.com/mkpaz/atlantafx/releases) page.
+* Copy `styles/target/AtlantaFX-${version}-scenebuilder.zip` to the SceneBuilder `app/` directory (e.g. `%HOMEPATH%/Local/SceneBuilder/app/` on Windows) or another directory depending on where you installed SceneBuilder application on your PC.
+* Open `SceneBuilder.cfg` in the SceneBuilder app directory and add the ZIP file to the beginning of the `app.classpath` variable, e.g.:
```text
- app.classpath=$APPDIR\AtlantaFX-${version}-scenebuilder.zip;$APPDIR\scenebuilder-18.0.0-all.jar
+ # beware about file separator (slash or backslash) depending on your OS
+ app.classpath=$APPDIR\AtlantaFX-${version}-scenebuilder.zip:$APPDIR\scenebuilder-18.0.0-all.jar
```
* Restart SceneBuilder.
Then you can select AtlantaFX themes in the menu `Preview -> Themes -> Caspian Embedded (FX2)`. The themes are mapped as follows:
-| SceneBuilder | Modifier | AtlantaFX Theme |
+| SceneBuilder | Caspian High Contrast (FX2) | AtlantaFX Theme |
|-----------------------------|-----------------------------|-----------------|
-| Caspian Embedded (FX2) | None | Primer Light |
-| Caspian Embedded (FX2) | Caspian High Contrast (FX2) | Primer Dark |
-| Caspian Embedded QVGA (FX2) | None | Nord Light |
-| Caspian Embedded QVGA (FX2) | Caspian High Contrast (FX2) | Nord Dark |
+| Caspian Embedded (FX2) | disabled | Primer Light |
+| Caspian Embedded (FX2) | enabled | Primer Dark |
+| Caspian Embedded QVGA (FX2) | disabled | Nord Light |
+| Caspian Embedded QVGA (FX2) | enabled | Nord Dark |
diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/SceneBuilderDialog.java b/sampler/src/main/java/atlantafx/sampler/page/general/SceneBuilderDialog.java
new file mode 100644
index 0000000..00ad4c9
--- /dev/null
+++ b/sampler/src/main/java/atlantafx/sampler/page/general/SceneBuilderDialog.java
@@ -0,0 +1,430 @@
+/* SPDX-License-Identifier: MIT */
+
+package atlantafx.sampler.page.general;
+
+import static atlantafx.base.theme.Styles.TEXT_CAPTION;
+import static atlantafx.base.theme.Styles.TEXT_MUTED;
+
+import atlantafx.base.controls.Spacer;
+import atlantafx.base.layout.DeckPane;
+import atlantafx.base.theme.Styles;
+import atlantafx.sampler.Resources;
+import atlantafx.sampler.event.BrowseEvent;
+import atlantafx.sampler.event.DefaultEventBus;
+import atlantafx.sampler.page.OverlayDialog;
+import atlantafx.sampler.page.general.SceneBuilderDialogModel.Screen;
+import atlantafx.sampler.util.NodeUtils;
+import java.io.File;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.scene.Cursor;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.Hyperlink;
+import javafx.scene.control.Label;
+import javafx.scene.control.OverrunStyle;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.Tooltip;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.FlowPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.stage.DirectoryChooser;
+import javafx.util.Duration;
+import org.kordamp.ikonli.javafx.FontIcon;
+import org.kordamp.ikonli.material2.Material2AL;
+import org.kordamp.ikonli.material2.Material2OutlinedAL;
+
+class SceneBuilderDialog extends OverlayDialog {
+
+ private final DeckPane deck;
+ private final Button backBtn;
+ private final Button forwardBtn;
+ private final Button closeBtn;
+
+ private Pane startScreen;
+ private Pane actionScreen;
+ private Pane themeScreen;
+ private Pane execScreen;
+ private Pane reportScreen;
+
+ private final SceneBuilderDialogModel model = new SceneBuilderDialogModel();
+
+ public SceneBuilderDialog() {
+ deck = createContent();
+
+ backBtn = new Button("Previous", new FontIcon(Material2AL.ARROW_BACK));
+
+ forwardBtn = new Button("Next", new FontIcon(Material2AL.ARROW_FORWARD));
+ forwardBtn.setContentDisplay(ContentDisplay.RIGHT);
+
+ closeBtn = new Button("Close");
+ NodeUtils.toggleVisibility(closeBtn, false);
+
+ footerBox.getChildren().setAll(backBtn, new Spacer(), forwardBtn, closeBtn);
+
+ setTitle("SceneBuilder Integration");
+ setContent(deck);
+ init();
+ }
+
+ private DeckPane createContent() {
+ startScreen = createStartScreen();
+ actionScreen = createActionScreen();
+ themeScreen = createThemeScreen();
+ execScreen = createExecScreen();
+ reportScreen = createReportScreen();
+
+ var deck = new DeckPane();
+ deck.addChildren(Insets.EMPTY, startScreen, actionScreen, themeScreen, execScreen, reportScreen);
+ deck.setAnimationDuration(Duration.millis(250));
+ deck.setId("scene-builder-wizard");
+
+ deck.setPrefSize(600, 440);
+
+ return deck;
+ }
+
+ private void init() {
+ deck.setTopNode(startScreen);
+
+ model.activeScreenProperty().addListener((obs, old, val) -> {
+ if (val == null) {
+ deck.resetTopNode();
+ return;
+ }
+
+ var nextScreen = getScreenView(val);
+ if (old == null || old.ordinal() < val.ordinal()) {
+ deck.swipeLeft(nextScreen);
+ } else {
+ deck.swipeRight(nextScreen);
+ }
+
+ if (val == Screen.REPORT) {
+ NodeUtils.toggleVisibility(closeBtn, true);
+ NodeUtils.toggleVisibility(forwardBtn, false);
+ } else {
+ NodeUtils.toggleVisibility(closeBtn, false);
+ NodeUtils.toggleVisibility(forwardBtn, true);
+ }
+ });
+
+ backBtn.setOnAction(e -> model.back());
+ backBtn.visibleProperty().bind(model.canGoBackProperty());
+
+ forwardBtn.setOnAction(e -> model.forward());
+ forwardBtn.disableProperty().bind(model.canGoForwardProperty().not());
+
+ closeBtn.setOnAction(e -> close());
+ }
+
+ void reset() {
+ model.reset();
+ }
+
+ private Pane getScreenView(Screen screen) {
+ return switch (screen) {
+ case START -> startScreen;
+ case ACTION -> actionScreen;
+ case THEME -> themeScreen;
+ case EXEC -> execScreen;
+ case REPORT -> reportScreen;
+ };
+ }
+
+ private Pane createStartScreen() {
+ var previewImg = new ImageView(new Image(
+ Resources.getResourceAsStream("images/scene-builder-in-action.jpg")
+ ));
+ previewImg.setFitWidth(280);
+ previewImg.setFitHeight(190);
+
+ var previewLbl = new Label("""
+ SceneBuilder is a visual layout tool that lets users quickly \
+ design JavaFX application user interfaces, without coding.
+ """);
+ previewLbl.setWrapText(true);
+
+ var downloadLnk = new Hyperlink("Get SceneBuilder");
+ downloadLnk.setGraphic(new FontIcon(Material2OutlinedAL.LINK));
+ downloadLnk.setOnAction(e -> DefaultEventBus.getInstance().publish(
+ new BrowseEvent(URI.create("https://gluonhq.com/products/scene-builder/"))
+ ));
+
+ var previewBox = new HBox(20, previewImg, new VBox(20, previewLbl, downloadLnk));
+ previewBox.setAlignment(Pos.TOP_LEFT);
+
+ var browseLbl = new Label("Select SceneBuilder installation directory:");
+ browseLbl.getStyleClass().addAll(TEXT_CAPTION, TEXT_MUTED);
+
+ var browseBtn = new Button("Browse", new FontIcon(Material2OutlinedAL.FOLDER));
+ browseBtn.setMinWidth(120);
+ browseBtn.setOnAction(e -> {
+ var dirChooser = new DirectoryChooser();
+ File dir = dirChooser.showDialog(getScene().getWindow());
+ if (dir != null) {
+ model.setInstallDir(dir.toPath());
+ }
+ });
+
+ var installDirLbl = new Label();
+ installDirLbl.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
+ installDirLbl.textProperty().bind(model.installDirProperty().map(
+ path -> path.toAbsolutePath().toString()
+ ));
+ HBox.setHgrow(installDirLbl, Priority.ALWAYS);
+
+ var installDirBox = new HBox(10, browseBtn, installDirLbl);
+ installDirBox.setAlignment(Pos.CENTER_LEFT);
+
+ var noticeLbl = new Label("""
+ You must have write permission to the directory. \
+ Installation files will be overwritten, but you can rollback changes using the same dialog again.
+ """);
+ noticeLbl.setWrapText(true);
+
+ // ~
+
+ var root = new VBox();
+ root.getChildren().setAll(
+ previewBox,
+ new Spacer(20, Orientation.VERTICAL),
+ browseLbl,
+ installDirBox,
+ noticeLbl
+ );
+ root.setAlignment(Pos.CENTER_LEFT);
+ root.getStyleClass().add("screen");
+
+ return root;
+ }
+
+ private Pane createActionScreen() {
+ var icon = new ImageView(new Image(
+ Resources.getResourceAsStream("images/question.png")
+ ));
+ icon.setFitWidth(64);
+ icon.setFitHeight(64);
+
+ var iconBox = new HBox(icon);
+ iconBox.setAlignment(Pos.CENTER);
+
+ var msgLbl = new Label("AtlantaFX theme pack is already installed.");
+
+ var updateRadio = new RadioButton("Update or install other themes");
+ updateRadio.setToggleGroup(model.getActionGroup());
+ updateRadio.setSelected(true);
+ updateRadio.setUserData(SceneBuilderDialogModel.ACTION_INSTALL);
+
+ var rollbackRadio = new RadioButton("Uninstall AtlantaFX themes from SceneBuilder");
+ rollbackRadio.setToggleGroup(model.getActionGroup());
+ rollbackRadio.setUserData(SceneBuilderDialogModel.ACTION_ROLLBACK);
+
+ // ~
+
+ var root = new VBox();
+ root.getChildren().setAll(
+ iconBox,
+ new Spacer(20, Orientation.VERTICAL),
+ msgLbl,
+ updateRadio,
+ rollbackRadio
+ );
+ root.setAlignment(Pos.CENTER_LEFT);
+ root.getStyleClass().add("screen");
+
+ return root;
+ }
+
+ private Pane createThemeScreen() {
+ var icon = new ImageView(new Image(
+ Resources.getResourceAsStream("images/color-palette.png")
+ ));
+ icon.setFitWidth(64);
+ icon.setFitHeight(64);
+
+ var iconBox = new HBox(icon);
+ iconBox.setAlignment(Pos.CENTER);
+
+ var msgLbl = new Label("Please select up to four themes to install.");
+
+ final var checkBoxes = new ArrayList();
+ final var listener = new ChangeListener() {
+
+ private int activeCount = 0;
+
+ @Override
+ public void changed(ObservableValue extends Boolean> obs, Boolean old, Boolean val) {
+ if (val) {
+ activeCount++;
+ if (activeCount == model.getSceneBuilderThemes().size() - 1) {
+ for (var cb : checkBoxes) {
+ if (!cb.isSelected()) {
+ cb.setDisable(true);
+ }
+ }
+ }
+ } else {
+ if (activeCount == model.getSceneBuilderThemes().size() - 1) {
+ for (var cb : checkBoxes) {
+ cb.setDisable(false);
+ }
+ }
+ activeCount--;
+ }
+
+ model.notifyThemeToggleStateChanged();
+ }
+ };
+
+ model.getThemes().forEach(toggle -> {
+ var cb = new CheckBox(toggle.getTheme().getName());
+ cb.selectedProperty().bindBidirectional(toggle.selectedProperty());
+ cb.setUserData(toggle.getTheme());
+ cb.setPrefWidth(250); // 2 columns, so half dialog size
+ cb.selectedProperty().addListener(listener);
+ checkBoxes.add(cb);
+ });
+
+ var checkBoxPane = new FlowPane(20, 10);
+ checkBoxPane.getChildren().setAll(checkBoxes);
+
+ var root = new VBox();
+ root.getChildren().setAll(
+ iconBox,
+ new Spacer(20, Orientation.VERTICAL),
+ msgLbl,
+ checkBoxPane
+ );
+ root.setAlignment(Pos.CENTER_LEFT);
+ root.getStyleClass().add("screen");
+
+ return root;
+ }
+
+ private Pane createExecScreen() {
+ var menuImg = new ImageView(new Image(
+ Resources.getResourceAsStream("images/scene-builder-themes.png")
+ ));
+ menuImg.setFitWidth(280);
+ menuImg.setFitHeight(210);
+ menuImg.setCursor(Cursor.HAND);
+
+ var test = new ImageView(new Image(
+ Resources.getResourceAsStream("images/scene-builder-themes.png")
+ ));
+ test.setPickOnBounds(true);
+
+ Tooltip tooltip = new Tooltip();
+ tooltip.setGraphic(test);
+
+ Tooltip.install(menuImg, tooltip);
+
+ var msgLbl = new Label("""
+ SceneBuilder doesn't support adding external themes. AtlantaFX themes will \
+ replace old and unused Caspian stylesheets.
+ """);
+ msgLbl.setWrapText(true);
+
+ var docsLbl = new Label("You can find more info about the process in the docs.");
+ docsLbl.setWrapText(true);
+
+ var docsLink = new Hyperlink("Documentation");
+ docsLink.setGraphic(new FontIcon(Material2OutlinedAL.LINK));
+ docsLink.setOnAction(e -> DefaultEventBus.getInstance().publish(
+ new BrowseEvent(URI.create("https://mkpaz.github.io/atlantafx/fxml/"))
+ ));
+
+ var imageBox = new HBox(20, menuImg, new VBox(20, msgLbl, docsLbl, docsLink));
+ imageBox.setAlignment(Pos.TOP_LEFT);
+
+ var mappingLbl = new Label("After the installation themes will be mapped as follows.");
+ mappingLbl.getStyleClass().addAll(TEXT_CAPTION, TEXT_MUTED);
+
+ final var mappingGrid = new GridPane();
+ mappingGrid.setHgap(20);
+ mappingGrid.setVgap(10);
+
+ model.themeMapProperty().addListener((obs, old, val) -> {
+ mappingGrid.getChildren().clear();
+ if (val != null) {
+ var idx = new AtomicInteger(0);
+ val.forEach((k, v) -> {
+ mappingGrid.add(new Label(k), 0, idx.get());
+ mappingGrid.add(new FontIcon(Material2AL.EAST), 1, idx.get());
+ mappingGrid.add(new Label(v), 2, idx.getAndIncrement());
+ });
+ }
+ });
+
+ var root = new VBox();
+ root.getChildren().setAll(
+ imageBox,
+ new Spacer(20, Orientation.VERTICAL),
+ mappingLbl,
+ mappingGrid
+ );
+ root.setAlignment(Pos.CENTER_LEFT);
+ root.getStyleClass().add("screen");
+
+ return root;
+ }
+
+ private Pane createReportScreen() {
+ var infoIcon = new ImageView(new Image(
+ Resources.getResourceAsStream("images/info.png")
+ ));
+ infoIcon.setFitWidth(64);
+ infoIcon.setFitHeight(64);
+
+ var warningIcon = new ImageView(new Image(
+ Resources.getResourceAsStream("images/warning.png")
+ ));
+ warningIcon.setFitWidth(64);
+ warningIcon.setFitHeight(64);
+
+ var iconBox = new HBox();
+ iconBox.setAlignment(Pos.CENTER);
+
+ var msgLbl = new Label();
+ msgLbl.setAlignment(Pos.TOP_CENTER);
+ msgLbl.getStyleClass().add(Styles.TITLE_4);
+ msgLbl.setMaxWidth(400);
+ msgLbl.setWrapText(true);
+
+ model.reportProperty().addListener((obs, old, val) -> {
+ if (val != null) {
+ iconBox.getChildren().setAll(val.error() ? warningIcon : infoIcon);
+ msgLbl.setText(val.message());
+ } else {
+ iconBox.getChildren().clear();
+ msgLbl.setText(null);
+ }
+ });
+
+ // ~
+
+ var root = new VBox();
+ root.getChildren().setAll(
+ iconBox,
+ new Spacer(20, Orientation.VERTICAL),
+ msgLbl
+ );
+ root.setAlignment(Pos.CENTER);
+ root.getStyleClass().add("screen");
+
+ return root;
+ }
+}
diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/SceneBuilderDialogModel.java b/sampler/src/main/java/atlantafx/sampler/page/general/SceneBuilderDialogModel.java
new file mode 100644
index 0000000..16c5e95
--- /dev/null
+++ b/sampler/src/main/java/atlantafx/sampler/page/general/SceneBuilderDialogModel.java
@@ -0,0 +1,340 @@
+package atlantafx.sampler.page.general;
+
+import atlantafx.base.theme.PrimerLight;
+import atlantafx.sampler.theme.SamplerTheme;
+import atlantafx.sampler.theme.SceneBuilderTheme;
+import atlantafx.sampler.theme.ThemeManager;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.concurrent.Task;
+import javafx.scene.control.ToggleGroup;
+import org.jetbrains.annotations.Nullable;
+
+class SceneBuilderDialogModel {
+
+ public enum Screen {
+ // order matters as it determines swipe direction when switching between screens
+ START, ACTION, THEME, EXEC, REPORT
+ }
+
+ static final String ACTION_INSTALL = "INSTALL";
+ static final String ACTION_ROLLBACK = "ROLLBACK";
+
+ public SceneBuilderDialogModel() {
+ // default constructor
+ }
+
+ private void changeScreen(@Nullable Transition transition) {
+ // null value means that forward action was blocked on the previous screen, so no transition required
+ if (transition == null) {
+ return;
+ }
+
+ canGoBack.set(transition.canGoBack());
+ canGoForward.set(transition.canGoForward());
+ if (transition.action() != null) {
+ transition.action().run();
+ }
+ activeScreen.set(transition.nextScreen());
+ }
+
+ private void updateThemeMapping() {
+ themeMap.set(getThemeMapping().entrySet().stream().collect(
+ Collectors.toMap(
+ e -> e.getKey().name(),
+ e -> e.getValue().getName(),
+ (e1, e2) -> e1,
+ LinkedHashMap::new
+ )
+ ));
+ }
+
+ private Map getThemeMapping() {
+ var sbIdx = 0;
+ var map = new LinkedHashMap();
+
+ for (int idx = 0; idx < themes.size() && sbIdx < sceneBuilderThemes.size(); idx++) {
+ var samplerTheme = themes.get(idx);
+ if (samplerTheme.isSelected()) {
+ var sbTheme = sceneBuilderThemes.get(sbIdx);
+ map.put(sbTheme, samplerTheme.getTheme());
+ sbIdx++;
+ }
+ }
+
+ return map;
+ }
+
+ private void installSelectedThemes() {
+ Objects.requireNonNull(installer, "SceneBuilder install directory must be selected first.");
+
+ var task = new Task() {
+ @Override
+ protected Void call() {
+ installer.install(getThemeMapping());
+ return null;
+ }
+ };
+ task.setOnSucceeded(x -> report.set(Report.info(
+ "AtlantaFX themes successfully installed.\nRestart SceneBuilder to apply changes.",
+ Screen.START
+ )));
+ task.setOnFailed(e -> report.set(Report.error(e.getSource().getException().getMessage(), Screen.EXEC)));
+
+ new Thread(task).start();
+ }
+
+ private void uninstallAll() {
+ Objects.requireNonNull(installer, "SceneBuilder install directory must be selected first.");
+
+ var task = new Task() {
+ @Override
+ protected Void call() {
+ installer.uninstall();
+ return null;
+ }
+ };
+ task.setOnSucceeded(x -> report.set(Report.info(
+ "AtlantaFX themes successfully uninstalled.\nRestart SceneBuilder to apply changes.",
+ Screen.START
+ )));
+ task.setOnFailed(e -> report.set(Report.error(e.getSource().getException().getMessage(), Screen.EXEC)));
+
+ new Thread(task).start();
+ }
+
+ private void requireSupportedAction() {
+ String action = (String) actionGroup.getSelectedToggle().getUserData();
+
+ if (action == null) {
+ throw new RuntimeException("Action must be selected.");
+ }
+
+ if (!ACTION_INSTALL.equals(action) && !ACTION_ROLLBACK.equals(action)) {
+ throw new RuntimeException("Unknown action: \"" + action + "\".");
+ }
+ }
+
+ private boolean isSelectedAction(String action) {
+ return action.equals(actionGroup.getSelectedToggle().getUserData());
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Properties //
+ ///////////////////////////////////////////////////////////////////////////
+
+ private final List sceneBuilderThemes = SceneBuilderTheme.CASPIAN_THEMES;
+
+ private final List themes = ThemeManager.getInstance().getRepository().getAll().stream()
+ .map(t -> new ThemeToggle(t, t.unwrap() instanceof PrimerLight))
+ .toList();
+
+ private final ToggleGroup actionGroup = new ToggleGroup();
+ private @Nullable SceneBuilderInstaller installer;
+
+ private final ReadOnlyObjectWrapper