Add SceneBuilder integration (#27)
@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- (Base) New `DeckPane` component with swipe and slide transition support.
|
||||||
- (CSS) 🚀 New MacOS-like Cupertino theme in light and dark variants.
|
- (CSS) 🚀 New MacOS-like Cupertino theme in light and dark variants.
|
||||||
- (CSS) 🚀 New [Dracula](https://ui.draculatheme.com/) theme.
|
- (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
|
### Improvements
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ module atlantafx.base {
|
|||||||
requires static org.jetbrains.annotations;
|
requires static org.jetbrains.annotations;
|
||||||
|
|
||||||
exports atlantafx.base.controls;
|
exports atlantafx.base.controls;
|
||||||
|
exports atlantafx.base.layout;
|
||||||
exports atlantafx.base.theme;
|
exports atlantafx.base.theme;
|
||||||
exports atlantafx.base.util;
|
exports atlantafx.base.util;
|
||||||
|
|
||||||
|
BIN
docs/docs/assets/images/scene-builder-integration.png
Normal file
After Width: | Height: | Size: 23 KiB |
@ -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 <ins>existing buttons load custom CSS files</ins>.
|
While SceneBuilder does not support adding custom themes, it is possible to overwrite looked-up CSS paths to make the <ins>existing buttons load custom CSS files</ins>.
|
||||||
|
|
||||||
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.
|
![Project structure](assets/images/scene-builder-integration.png)
|
||||||
* 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.:
|
### 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
|
```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.
|
* Restart SceneBuilder.
|
||||||
|
|
||||||
Then you can select AtlantaFX themes in the menu `Preview -> Themes -> Caspian Embedded (FX2)`. The themes are mapped as follows:
|
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) | disabled | Primer Light |
|
||||||
| Caspian Embedded (FX2) | Caspian High Contrast (FX2) | Primer Dark |
|
| Caspian Embedded (FX2) | enabled | Primer Dark |
|
||||||
| Caspian Embedded QVGA (FX2) | None | Nord Light |
|
| Caspian Embedded QVGA (FX2) | disabled | Nord Light |
|
||||||
| Caspian Embedded QVGA (FX2) | Caspian High Contrast (FX2) | Nord Dark |
|
| Caspian Embedded QVGA (FX2) | enabled | Nord Dark |
|
||||||
|
@ -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<DeckPane> {
|
||||||
|
|
||||||
|
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<CheckBox>();
|
||||||
|
final var listener = new ChangeListener<Boolean>() {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<SceneBuilderTheme, SamplerTheme> getThemeMapping() {
|
||||||
|
var sbIdx = 0;
|
||||||
|
var map = new LinkedHashMap<SceneBuilderTheme, SamplerTheme>();
|
||||||
|
|
||||||
|
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<Void>() {
|
||||||
|
@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<Void>() {
|
||||||
|
@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<SceneBuilderTheme> sceneBuilderThemes = SceneBuilderTheme.CASPIAN_THEMES;
|
||||||
|
|
||||||
|
private final List<ThemeToggle> 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<Map<String, String>> themeMap = new ReadOnlyObjectWrapper<>();
|
||||||
|
private final ReadOnlyObjectWrapper<Path> installDir = new ReadOnlyObjectWrapper<>();
|
||||||
|
private final ReadOnlyObjectWrapper<Screen> activeScreen = new ReadOnlyObjectWrapper<>(Screen.START);
|
||||||
|
private final ReadOnlyObjectWrapper<Report> report = new ReadOnlyObjectWrapper<>(null);
|
||||||
|
private final ReadOnlyBooleanWrapper canGoBack = new ReadOnlyBooleanWrapper(false);
|
||||||
|
// block initially until user selects installation directory
|
||||||
|
private final ReadOnlyBooleanWrapper canGoForward = new ReadOnlyBooleanWrapper(false);
|
||||||
|
|
||||||
|
public List<SceneBuilderTheme> getSceneBuilderThemes() {
|
||||||
|
return sceneBuilderThemes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ThemeToggle> getThemes() {
|
||||||
|
return themes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ToggleGroup getActionGroup() {
|
||||||
|
return actionGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<Map<String, String>> themeMapProperty() {
|
||||||
|
return themeMap.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<Path> installDirProperty() {
|
||||||
|
return installDir.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<Screen> activeScreenProperty() {
|
||||||
|
return activeScreen.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<Report> reportProperty() {
|
||||||
|
return report.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyBooleanProperty canGoBackProperty() {
|
||||||
|
return canGoBack.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyBooleanProperty canGoForwardProperty() {
|
||||||
|
return canGoForward.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Commands //
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void setInstallDir(Path path) {
|
||||||
|
Objects.requireNonNull(path);
|
||||||
|
installDir.set(path);
|
||||||
|
installer = new SceneBuilderInstaller(path);
|
||||||
|
canGoForward.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyThemeToggleStateChanged() {
|
||||||
|
var selectedCount = themes.stream()
|
||||||
|
.filter(ThemeToggle::isSelected)
|
||||||
|
.count();
|
||||||
|
canGoForward.set(selectedCount > 0 && selectedCount <= sceneBuilderThemes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
// go to the start screen, but keep install dir and form fill
|
||||||
|
activeScreen.set(Screen.START);
|
||||||
|
canGoBack.set(false);
|
||||||
|
canGoForward.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void back() {
|
||||||
|
var transition = switch (activeScreen.get()) {
|
||||||
|
case START -> null;
|
||||||
|
case ACTION -> new Transition(Screen.START, false, true);
|
||||||
|
case THEME -> {
|
||||||
|
Objects.requireNonNull(installer, "SceneBuilder install directory must be selected first.");
|
||||||
|
|
||||||
|
yield installer.isThemePackInstalled()
|
||||||
|
? new Transition(Screen.ACTION, true, true)
|
||||||
|
: new Transition(Screen.START, false, true);
|
||||||
|
}
|
||||||
|
case EXEC -> isSelectedAction(ACTION_INSTALL)
|
||||||
|
? new Transition(Screen.THEME, true, true)
|
||||||
|
: new Transition(Screen.ACTION, true, true);
|
||||||
|
case REPORT -> {
|
||||||
|
var returnScreen = report.get().returnScreen();
|
||||||
|
yield new Transition(returnScreen, returnScreen != Screen.START, true, () -> report.set(null));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
changeScreen(transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forward() {
|
||||||
|
var transition = switch (activeScreen.get()) {
|
||||||
|
case START -> {
|
||||||
|
Objects.requireNonNull(installer, "SceneBuilder install directory must be selected first.");
|
||||||
|
|
||||||
|
if (!installer.isValidDir()) {
|
||||||
|
yield new Transition(Screen.REPORT, true, false, () -> report.set(
|
||||||
|
Report.error("Selected directory doesn't look like SceneBuilder installation directory.")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!installer.hasUserWritePermission()) {
|
||||||
|
yield new Transition(Screen.REPORT, true, false, () -> report.set(
|
||||||
|
Report.error("You don't have permission to write into installation directory.")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
yield new Transition(
|
||||||
|
installer.isThemePackInstalled() ? Screen.ACTION : Screen.THEME, true, true,
|
||||||
|
() -> actionGroup.selectToggle(actionGroup.getToggles().get(0)) // reset action
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case ACTION -> {
|
||||||
|
// action must be selected before leaving this screen (fail first)
|
||||||
|
requireSupportedAction();
|
||||||
|
|
||||||
|
if (isSelectedAction(ACTION_ROLLBACK)) {
|
||||||
|
yield new Transition(Screen.REPORT, true, false, this::uninstallAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield new Transition(Screen.THEME, true, true);
|
||||||
|
}
|
||||||
|
case THEME -> new Transition(Screen.EXEC, true, true, this::updateThemeMapping);
|
||||||
|
case EXEC -> {
|
||||||
|
requireSupportedAction();
|
||||||
|
|
||||||
|
if (isSelectedAction(ACTION_INSTALL)) {
|
||||||
|
yield new Transition(Screen.REPORT, true, false, this::installSelectedThemes);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield null;
|
||||||
|
}
|
||||||
|
case REPORT -> null;
|
||||||
|
};
|
||||||
|
|
||||||
|
changeScreen(transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public record Report(String message, Screen returnScreen, boolean error) {
|
||||||
|
|
||||||
|
public Report {
|
||||||
|
Objects.requireNonNull(message);
|
||||||
|
Objects.requireNonNull(returnScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Report info(String message) {
|
||||||
|
return info(message, Screen.EXEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Report info(String message, Screen returnScreen) {
|
||||||
|
return new Report(message, returnScreen, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Report error(String message) {
|
||||||
|
return error(message, Screen.START);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Report error(String message, Screen returnScreen) {
|
||||||
|
return new Report(message, returnScreen, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ThemeToggle {
|
||||||
|
|
||||||
|
private final SamplerTheme theme;
|
||||||
|
private final BooleanProperty selected = new SimpleBooleanProperty();
|
||||||
|
|
||||||
|
public ThemeToggle(SamplerTheme theme, boolean selected) {
|
||||||
|
this.theme = Objects.requireNonNull(theme);
|
||||||
|
this.selected.set(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SamplerTheme getTheme() {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSelected() {
|
||||||
|
return selected.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty selectedProperty() {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Transition(Screen nextScreen, boolean canGoBack, boolean canGoForward, @Nullable Runnable action) {
|
||||||
|
|
||||||
|
public Transition(Screen nextScreen, boolean canGoBack, boolean canGoForward) {
|
||||||
|
this(nextScreen, canGoBack, canGoForward, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,234 @@
|
|||||||
|
package atlantafx.sampler.page.general;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import atlantafx.sampler.theme.SamplerTheme;
|
||||||
|
import atlantafx.sampler.theme.SceneBuilderTheme;
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ListIterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
class SceneBuilderInstaller {
|
||||||
|
|
||||||
|
private static final String THEME_PACK_FILE_NAME = "atlantafx-scene-builder.zip";
|
||||||
|
|
||||||
|
private final Path sceneBuilderDir;
|
||||||
|
|
||||||
|
public SceneBuilderInstaller(Path dir) {
|
||||||
|
this.sceneBuilderDir = Objects.requireNonNull(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUserWritePermission() {
|
||||||
|
return Files.isWritable(getConfigDir()) && Files.isWritable(getConfigFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValidDir() {
|
||||||
|
var cfgDir = getConfigDir();
|
||||||
|
var cfgFile = getConfigFile();
|
||||||
|
return Files.exists(cfgDir) && Files.isDirectory(cfgDir)
|
||||||
|
&& Files.exists(cfgFile) && Files.isRegularFile(cfgFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isThemePackInstalled() {
|
||||||
|
try {
|
||||||
|
String cfg = Files.readString(getConfigFile(), UTF_8);
|
||||||
|
Path themePack = getThemePack();
|
||||||
|
return cfg.contains(THEME_PACK_FILE_NAME) && Files.exists(themePack) && Files.isRegularFile(themePack);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("StringSplitter")
|
||||||
|
public void install(Map<SceneBuilderTheme, SamplerTheme> themes) {
|
||||||
|
Objects.requireNonNull(themes);
|
||||||
|
|
||||||
|
if (themes.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write theme pack archive to install dir
|
||||||
|
File zipFile = getThemePack().toFile();
|
||||||
|
try (var fos = new FileOutputStream(zipFile);
|
||||||
|
var bos = new BufferedOutputStream(fos);
|
||||||
|
var out = new ZipOutputStream(bos)) {
|
||||||
|
|
||||||
|
var readme = new StringBuilder();
|
||||||
|
readme
|
||||||
|
.append("This file was auto-generated by AtlantaFX Sampler v")
|
||||||
|
.append(System.getProperty("app.version"))
|
||||||
|
.append(".\n\n")
|
||||||
|
.append("Installed themes:\n\n");
|
||||||
|
|
||||||
|
for (var theme : themes.entrySet()) {
|
||||||
|
var zipPath = theme.getKey().url();
|
||||||
|
|
||||||
|
readme
|
||||||
|
.append(String.format("%-45s", theme.getKey().name()))
|
||||||
|
.append(" >> ")
|
||||||
|
.append(theme.getValue().getName())
|
||||||
|
.append("\n");
|
||||||
|
|
||||||
|
writeToZip(out, zipPath, theme.getValue().getResource().getInputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
writeToZip(out, "readme.txt", new ByteArrayInputStream(readme.toString().getBytes(UTF_8)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Unable to write theme pack to the SceneBuilder installation directory.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update config file
|
||||||
|
try {
|
||||||
|
List<String> cfgData = Files.readAllLines(getConfigFile(), UTF_8);
|
||||||
|
|
||||||
|
// already updated
|
||||||
|
if (cfgData.stream().anyMatch(s -> s.contains(THEME_PACK_FILE_NAME))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
backupConfig();
|
||||||
|
|
||||||
|
ListIterator<String> it = cfgData.listIterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
var line = it.next();
|
||||||
|
if (line != null && line.startsWith("app.classpath")) {
|
||||||
|
var kv = line.split("=");
|
||||||
|
|
||||||
|
if (kv.length != 2) {
|
||||||
|
throw new RuntimeException("Unexpected value in SceneBuilder config file: \"" + line + "\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
it.set(kv[0] + "=$APPDIR" + File.separator + THEME_PACK_FILE_NAME + ":" + kv[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.writeString(
|
||||||
|
getConfigFile(),
|
||||||
|
String.join("\n", cfgData), // System.lineSeparator() ?
|
||||||
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.TRUNCATE_EXISTING
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Unable to update SceneBuilder config file.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeToZip(ZipOutputStream out, String zipPath, InputStream in) throws IOException {
|
||||||
|
var entry = new ZipEntry(zipPath);
|
||||||
|
try (in) {
|
||||||
|
out.putNextEntry(entry);
|
||||||
|
|
||||||
|
byte[] bytes = new byte[1024];
|
||||||
|
int count = in.read(bytes);
|
||||||
|
while (count > -1) {
|
||||||
|
out.write(bytes, 0, count);
|
||||||
|
count = in.read(bytes);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
out.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void uninstall() {
|
||||||
|
var cfg = getConfigFile();
|
||||||
|
var backup = getBackupConfigFile();
|
||||||
|
|
||||||
|
// rollback config file
|
||||||
|
if (Files.exists(backup)) {
|
||||||
|
// easy way
|
||||||
|
copyFile(backup, cfg, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} else {
|
||||||
|
// fallback (probably not needed, but should do no harm)
|
||||||
|
try {
|
||||||
|
List<String> cfgData = Files.readAllLines(getConfigFile(), UTF_8);
|
||||||
|
|
||||||
|
// not present
|
||||||
|
if (cfgData.stream().noneMatch(s -> s.contains(THEME_PACK_FILE_NAME))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListIterator<String> it = cfgData.listIterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
var line = it.next();
|
||||||
|
if (line != null && line.startsWith("app.classpath")) {
|
||||||
|
it.set(line.replace("$APPDIR" + File.separator + THEME_PACK_FILE_NAME + ":", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.writeString(
|
||||||
|
getConfigFile(),
|
||||||
|
String.join("\n", cfgData), // System.lineSeparator() ?
|
||||||
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.TRUNCATE_EXISTING
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Unable to update SceneBuilder config file.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove theme pack and backup
|
||||||
|
deleteFile(getThemePack());
|
||||||
|
deleteFile(backup);
|
||||||
|
}
|
||||||
|
|
||||||
|
void backupConfig() {
|
||||||
|
var cfg = getConfigFile();
|
||||||
|
var backup = getBackupConfigFile();
|
||||||
|
|
||||||
|
if (!Files.exists(backup)) {
|
||||||
|
copyFile(cfg, backup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyFile(Path source, Path dest, StandardCopyOption... options) {
|
||||||
|
Objects.requireNonNull(source);
|
||||||
|
Objects.requireNonNull(dest);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.copy(source, dest, options);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Unable to copy \"" + source + "\" to \"" + dest + "\".", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteFile(Path path) {
|
||||||
|
Objects.requireNonNull(path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Files.exists(path)) {
|
||||||
|
Files.delete(path);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getConfigDir() {
|
||||||
|
return sceneBuilderDir.resolve("lib/app");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getConfigFile() {
|
||||||
|
return getConfigDir().resolve("SceneBuilder.cfg");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getBackupConfigFile() {
|
||||||
|
return getConfigDir().resolve("SceneBuilder.cfg.atlantafx.backup");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getThemePack() {
|
||||||
|
return getConfigDir().resolve(THEME_PACK_FILE_NAME);
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
|
|||||||
import static atlantafx.sampler.util.Controls.hyperlink;
|
import static atlantafx.sampler.util.Controls.hyperlink;
|
||||||
|
|
||||||
import atlantafx.base.theme.Styles;
|
import atlantafx.base.theme.Styles;
|
||||||
|
import atlantafx.sampler.Resources;
|
||||||
import atlantafx.sampler.event.DefaultEventBus;
|
import atlantafx.sampler.event.DefaultEventBus;
|
||||||
import atlantafx.sampler.event.ThemeEvent;
|
import atlantafx.sampler.event.ThemeEvent;
|
||||||
import atlantafx.sampler.page.AbstractPage;
|
import atlantafx.sampler.page.AbstractPage;
|
||||||
@ -25,6 +26,8 @@ import javafx.scene.control.Button;
|
|||||||
import javafx.scene.control.ChoiceBox;
|
import javafx.scene.control.ChoiceBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
@ -38,7 +41,11 @@ import org.kordamp.ikonli.material2.Material2OutlinedMZ;
|
|||||||
public class ThemePage extends AbstractPage {
|
public class ThemePage extends AbstractPage {
|
||||||
|
|
||||||
public static final String NAME = "Theme";
|
public static final String NAME = "Theme";
|
||||||
|
|
||||||
private static final ThemeManager TM = ThemeManager.getInstance();
|
private static final ThemeManager TM = ThemeManager.getInstance();
|
||||||
|
private static final Image SCENE_BUILDER_ICON = new Image(
|
||||||
|
Resources.getResourceAsStream("images/scene-builder_32.png")
|
||||||
|
);
|
||||||
|
|
||||||
private final Consumer<ColorPaletteBlock> colorBlockActionHandler = colorBlock -> {
|
private final Consumer<ColorPaletteBlock> colorBlockActionHandler = colorBlock -> {
|
||||||
ContrastCheckerDialog dialog = getOrCreateContrastCheckerDialog();
|
ContrastCheckerDialog dialog = getOrCreateContrastCheckerDialog();
|
||||||
@ -58,6 +65,7 @@ public class ThemePage extends AbstractPage {
|
|||||||
|
|
||||||
private ThemeRepoManagerDialog themeRepoManagerDialog;
|
private ThemeRepoManagerDialog themeRepoManagerDialog;
|
||||||
private ContrastCheckerDialog contrastCheckerDialog;
|
private ContrastCheckerDialog contrastCheckerDialog;
|
||||||
|
private SceneBuilderDialog sceneBuilderDialog;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@ -130,6 +138,14 @@ public class ThemePage extends AbstractPage {
|
|||||||
|
|
||||||
var accentSelector = new AccentColorSelector();
|
var accentSelector = new AccentColorSelector();
|
||||||
|
|
||||||
|
var sceneBuilderBtn = new Button("SceneBuilder Integration");
|
||||||
|
sceneBuilderBtn.setGraphic(new ImageView(SCENE_BUILDER_ICON));
|
||||||
|
sceneBuilderBtn.setOnAction(e -> {
|
||||||
|
SceneBuilderDialog dialog = getOrCreateScneBuilderDialog();
|
||||||
|
overlay.setContent(dialog, HPos.CENTER);
|
||||||
|
overlay.toFront();
|
||||||
|
});
|
||||||
|
|
||||||
// ~
|
// ~
|
||||||
|
|
||||||
var grid = new GridPane();
|
var grid = new GridPane();
|
||||||
@ -141,6 +157,7 @@ public class ThemePage extends AbstractPage {
|
|||||||
grid.add(themeRepoBtn, 2, 0);
|
grid.add(themeRepoBtn, 2, 0);
|
||||||
grid.add(new Label("Accent color"), 0, 1);
|
grid.add(new Label("Accent color"), 0, 1);
|
||||||
grid.add(accentSelector, 1, 1);
|
grid.add(accentSelector, 1, 1);
|
||||||
|
grid.add(sceneBuilderBtn, 0, 2, GridPane.REMAINING, 1);
|
||||||
|
|
||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
@ -208,4 +225,18 @@ public class ThemePage extends AbstractPage {
|
|||||||
|
|
||||||
return contrastCheckerDialog;
|
return contrastCheckerDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SceneBuilderDialog getOrCreateScneBuilderDialog() {
|
||||||
|
if (sceneBuilderDialog == null) {
|
||||||
|
sceneBuilderDialog = new SceneBuilderDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneBuilderDialog.setOnCloseRequest(() -> {
|
||||||
|
overlay.removeContent();
|
||||||
|
overlay.toBack();
|
||||||
|
sceneBuilderDialog.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
return sceneBuilderDialog;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ public final class SamplerTheme implements Theme {
|
|||||||
// any external file path must have "file://" prefix
|
// any external file path must have "file://" prefix
|
||||||
@Override
|
@Override
|
||||||
public String getUserAgentStylesheet() {
|
public String getUserAgentStylesheet() {
|
||||||
return IS_DEV_MODE ? DUMMY_STYLESHEET : getThemeFile().toURI().toString();
|
return IS_DEV_MODE ? DUMMY_STYLESHEET : getResource().toURI().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -93,7 +93,7 @@ public final class SamplerTheme implements Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> getAllStylesheets() {
|
public Set<String> getAllStylesheets() {
|
||||||
return IS_DEV_MODE ? merge(getThemeFile().toURI().toString(), APP_STYLESHEETS) : Set.of(APP_STYLESHEETS);
|
return IS_DEV_MODE ? merge(getResource().toURI().toString(), APP_STYLESHEETS) : Set.of(APP_STYLESHEETS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks whether wrapped theme is a project theme or user external theme.
|
// Checks whether wrapped theme is a project theme or user external theme.
|
||||||
@ -105,7 +105,7 @@ public final class SamplerTheme implements Theme {
|
|||||||
// - minified CSS files are not supported
|
// - minified CSS files are not supported
|
||||||
// - only first PARSE_LIMIT lines will be read
|
// - only first PARSE_LIMIT lines will be read
|
||||||
public Map<String, String> parseColors() throws IOException {
|
public Map<String, String> parseColors() throws IOException {
|
||||||
FileResource file = getThemeFile();
|
FileResource file = getResource();
|
||||||
return file.internal() ? parseColorsForClasspath(file) : parseColorsForFilesystem(file);
|
return file.internal() ? parseColorsForClasspath(file) : parseColorsForFilesystem(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,10 +162,10 @@ public final class SamplerTheme implements Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getPath() {
|
public String getPath() {
|
||||||
return getThemeFile().toPath().toString();
|
return getResource().toPath().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private FileResource getThemeFile() {
|
public FileResource getResource() {
|
||||||
if (!isProjectTheme()) {
|
if (!isProjectTheme()) {
|
||||||
return FileResource.createExternal(theme.getUserAgentStylesheet());
|
return FileResource.createExternal(theme.getUserAgentStylesheet());
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package atlantafx.sampler.theme;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record SceneBuilderTheme(String name, String url) {
|
||||||
|
|
||||||
|
public static final List<SceneBuilderTheme> CASPIAN_THEMES = List.of(
|
||||||
|
new SceneBuilderTheme(
|
||||||
|
"Caspian Embedded (FX2)",
|
||||||
|
"com/oracle/javafx/scenebuilder/kit/util/css/caspian/caspian-embedded.css"
|
||||||
|
),
|
||||||
|
new SceneBuilderTheme(
|
||||||
|
"Caspian Embedded QVGA (FX2)",
|
||||||
|
"com/oracle/javafx/scenebuilder/kit/util/css/caspian/caspian-embedded-qvga.css"
|
||||||
|
),
|
||||||
|
new SceneBuilderTheme(
|
||||||
|
"Caspian High Contrast Embedded (FX2)",
|
||||||
|
"com/oracle/javafx/scenebuilder/kit/util/css/caspian/caspian-embedded-highContrast.css"
|
||||||
|
),
|
||||||
|
new SceneBuilderTheme(
|
||||||
|
"Caspian High Contrast Embedded QVGA (FX2)",
|
||||||
|
"com/oracle/javafx/scenebuilder/kit/util/css/caspian/caspian-embedded-qvga-highContrast.css"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
public SceneBuilderTheme {
|
||||||
|
Objects.requireNonNull(name);
|
||||||
|
Objects.requireNonNull(url);
|
||||||
|
}
|
||||||
|
}
|
@ -2,4 +2,5 @@
|
|||||||
|
|
||||||
@use "fonts";
|
@use "fonts";
|
||||||
@use "root";
|
@use "root";
|
||||||
|
@use "scene-builder-wizard";
|
||||||
@use "util";
|
@use "util";
|
7
sampler/src/main/resources/atlantafx/sampler/assets/styles/scss/general/_scene-builder-wizard.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#scene-builder-wizard {
|
||||||
|
.screen {
|
||||||
|
-fx-padding: 10px 20px 10px 20px;
|
||||||
|
-fx-spacing: 10px;
|
||||||
|
-fx-background-color: -color-bg-default;
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 11 KiB |
BIN
sampler/src/main/resources/atlantafx/sampler/images/info.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
sampler/src/main/resources/atlantafx/sampler/images/question.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 1.8 KiB |
BIN
sampler/src/main/resources/atlantafx/sampler/images/warning.png
Normal file
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,29 @@
|
|||||||
|
package atlantafx.sampler.page.general;
|
||||||
|
|
||||||
|
import atlantafx.base.theme.PrimerDark;
|
||||||
|
import atlantafx.base.theme.PrimerLight;
|
||||||
|
import atlantafx.sampler.theme.SamplerTheme;
|
||||||
|
import atlantafx.sampler.theme.SceneBuilderTheme;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SceneBuilderInstallerTest {
|
||||||
|
|
||||||
|
private static final Path INSTALL_DIR = Paths.get("/opt/scenebuilder");
|
||||||
|
|
||||||
|
//@Test
|
||||||
|
public void testInstall() {
|
||||||
|
var installer = new SceneBuilderInstaller(INSTALL_DIR);
|
||||||
|
installer.install(Map.of(
|
||||||
|
SceneBuilderTheme.CASPIAN_THEMES.get(0), new SamplerTheme(new PrimerLight()),
|
||||||
|
SceneBuilderTheme.CASPIAN_THEMES.get(1), new SamplerTheme(new PrimerDark())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
//@Test
|
||||||
|
public void testUninstall() {
|
||||||
|
var installer = new SceneBuilderInstaller(INSTALL_DIR);
|
||||||
|
installer.uninstall();
|
||||||
|
}
|
||||||
|
}
|