Add SceneBuilder integration (#27)
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
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>.
|
||||
|
||||
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 |
|
||||
|
@ -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 atlantafx.base.theme.Styles;
|
||||
import atlantafx.sampler.Resources;
|
||||
import atlantafx.sampler.event.DefaultEventBus;
|
||||
import atlantafx.sampler.event.ThemeEvent;
|
||||
import atlantafx.sampler.page.AbstractPage;
|
||||
@ -25,6 +26,8 @@ import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ChoiceBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
@ -38,7 +41,11 @@ import org.kordamp.ikonli.material2.Material2OutlinedMZ;
|
||||
public class ThemePage extends AbstractPage {
|
||||
|
||||
public static final String NAME = "Theme";
|
||||
|
||||
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 -> {
|
||||
ContrastCheckerDialog dialog = getOrCreateContrastCheckerDialog();
|
||||
@ -58,6 +65,7 @@ public class ThemePage extends AbstractPage {
|
||||
|
||||
private ThemeRepoManagerDialog themeRepoManagerDialog;
|
||||
private ContrastCheckerDialog contrastCheckerDialog;
|
||||
private SceneBuilderDialog sceneBuilderDialog;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@ -130,6 +138,14 @@ public class ThemePage extends AbstractPage {
|
||||
|
||||
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();
|
||||
@ -141,6 +157,7 @@ public class ThemePage extends AbstractPage {
|
||||
grid.add(themeRepoBtn, 2, 0);
|
||||
grid.add(new Label("Accent color"), 0, 1);
|
||||
grid.add(accentSelector, 1, 1);
|
||||
grid.add(sceneBuilderBtn, 0, 2, GridPane.REMAINING, 1);
|
||||
|
||||
return grid;
|
||||
}
|
||||
@ -208,4 +225,18 @@ public class ThemePage extends AbstractPage {
|
||||
|
||||
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
|
||||
@Override
|
||||
public String getUserAgentStylesheet() {
|
||||
return IS_DEV_MODE ? DUMMY_STYLESHEET : getThemeFile().toURI().toString();
|
||||
return IS_DEV_MODE ? DUMMY_STYLESHEET : getResource().toURI().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -93,7 +93,7 @@ public final class SamplerTheme implements Theme {
|
||||
}
|
||||
|
||||
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.
|
||||
@ -105,7 +105,7 @@ public final class SamplerTheme implements Theme {
|
||||
// - minified CSS files are not supported
|
||||
// - only first PARSE_LIMIT lines will be read
|
||||
public Map<String, String> parseColors() throws IOException {
|
||||
FileResource file = getThemeFile();
|
||||
FileResource file = getResource();
|
||||
return file.internal() ? parseColorsForClasspath(file) : parseColorsForFilesystem(file);
|
||||
}
|
||||
|
||||
@ -162,10 +162,10 @@ public final class SamplerTheme implements Theme {
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return getThemeFile().toPath().toString();
|
||||
return getResource().toPath().toString();
|
||||
}
|
||||
|
||||
private FileResource getThemeFile() {
|
||||
public FileResource getResource() {
|
||||
if (!isProjectTheme()) {
|
||||
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 "root";
|
||||
@use "scene-builder-wizard";
|
||||
@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();
|
||||
}
|
||||
}
|