Improve Sampler layout
- Refactor to MVVM. - Add fancy top bar. - Add keyboard navigation for sidebar. - Add hotkeys support.
This commit is contained in:
parent
1c46a7a5d5
commit
978577dc6a
@ -3,6 +3,7 @@ package atlantafx.sampler;
|
||||
|
||||
import atlantafx.sampler.event.BrowseEvent;
|
||||
import atlantafx.sampler.event.DefaultEventBus;
|
||||
import atlantafx.sampler.event.HotkeyEvent;
|
||||
import atlantafx.sampler.event.Listener;
|
||||
import atlantafx.sampler.layout.ApplicationWindow;
|
||||
import atlantafx.sampler.theme.ThemeManager;
|
||||
@ -14,12 +15,17 @@ import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
@ -30,6 +36,10 @@ public class Launcher extends Application {
|
||||
Resources.getPropertyOrEnv("atlantafx.mode", "ATLANTAFX_MODE")
|
||||
);
|
||||
|
||||
public static final List<KeyCodeCombination> SUPPORTED_HOTKEYS = List.of(
|
||||
new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN)
|
||||
);
|
||||
|
||||
private static class DefaultExceptionHandler implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
@Override
|
||||
@ -45,14 +55,15 @@ public class Launcher extends Application {
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
Thread.currentThread().setUncaughtExceptionHandler(new DefaultExceptionHandler());
|
||||
loadApplicationProperties();
|
||||
|
||||
if (IS_DEV_MODE) {
|
||||
System.out.println("[WARNING] Application is running in development mode.");
|
||||
}
|
||||
|
||||
loadApplicationProperties();
|
||||
var root = new ApplicationWindow();
|
||||
var scene = new Scene(root, 1200, 768);
|
||||
scene.setOnKeyPressed(this::dispatchHotkeys);
|
||||
|
||||
var tm = ThemeManager.getInstance();
|
||||
tm.setScene(scene);
|
||||
@ -119,6 +130,15 @@ public class Launcher extends Application {
|
||||
CSSFX.start(scene);
|
||||
}
|
||||
|
||||
private void dispatchHotkeys(KeyEvent event) {
|
||||
for (KeyCodeCombination k : SUPPORTED_HOTKEYS) {
|
||||
if (k.match(event)) {
|
||||
DefaultEventBus.getInstance().publish(new HotkeyEvent(k));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Listener
|
||||
private void onBrowseEvent(BrowseEvent event) {
|
||||
getHostServices().showDocument(event.getUri().toString());
|
||||
|
@ -19,6 +19,6 @@ public class BrowseEvent extends Event {
|
||||
public String toString() {
|
||||
return "BrowseEvent{" +
|
||||
"uri=" + uri +
|
||||
'}';
|
||||
"} " + super.toString();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.event;
|
||||
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
|
||||
public class HotkeyEvent extends Event {
|
||||
|
||||
private final KeyCodeCombination keys;
|
||||
|
||||
public HotkeyEvent(KeyCodeCombination keys) {
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
public KeyCodeCombination getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HotkeyEvent{" +
|
||||
"keys=" + keys +
|
||||
"} " + super.toString();
|
||||
}
|
||||
}
|
@ -23,4 +23,11 @@ public class ThemeEvent extends Event {
|
||||
THEME_ADD,
|
||||
THEME_REMOVE
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ThemeEvent{" +
|
||||
"eventType=" + eventType +
|
||||
"} " + super.toString();
|
||||
}
|
||||
}
|
||||
|
136
sampler/src/main/java/atlantafx/sampler/layout/HeaderBar.java
Normal file
136
sampler/src/main/java/atlantafx/sampler/layout/HeaderBar.java
Normal file
@ -0,0 +1,136 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.layout;
|
||||
|
||||
import atlantafx.base.controls.CustomTextField;
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import atlantafx.sampler.event.BrowseEvent;
|
||||
import atlantafx.sampler.event.DefaultEventBus;
|
||||
import atlantafx.sampler.event.HotkeyEvent;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCombination.ModifierValue;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.kordamp.ikonli.Ikon;
|
||||
import org.kordamp.ikonli.feather.Feather;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
import org.kordamp.ikonli.material2.Material2MZ;
|
||||
import org.kordamp.ikonli.material2.Material2OutlinedMZ;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static atlantafx.base.theme.Styles.*;
|
||||
import static atlantafx.sampler.Launcher.IS_DEV_MODE;
|
||||
import static atlantafx.sampler.layout.MainLayer.SIDEBAR_WIDTH;
|
||||
|
||||
class HeaderBar extends HBox {
|
||||
|
||||
private static final int HEADER_HEIGHT = 50;
|
||||
private static final Ikon ICON_CODE = Feather.CODE;
|
||||
private static final Ikon ICON_SAMPLE = Feather.LAYOUT;
|
||||
|
||||
private final MainModel model;
|
||||
private Consumer<Node> quickConfigActionHandler;
|
||||
|
||||
public HeaderBar(MainModel model) {
|
||||
super();
|
||||
this.model = model;
|
||||
createView();
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
var logoLabel = new Label("AtlantaFX");
|
||||
logoLabel.getStyleClass().addAll(TITLE_3);
|
||||
|
||||
var versionLabel = new Label(System.getProperty("app.version"));
|
||||
versionLabel.getStyleClass().addAll("version", TEXT_SMALL);
|
||||
|
||||
var githubLink = new FontIcon(Feather.GITHUB);
|
||||
githubLink.getStyleClass().addAll("github");
|
||||
githubLink.setOnMouseClicked(e -> {
|
||||
var homepage = System.getProperty("app.homepage");
|
||||
if (homepage != null) {
|
||||
DefaultEventBus.getInstance().publish(new BrowseEvent(URI.create(homepage)));
|
||||
}
|
||||
});
|
||||
|
||||
var logoBox = new HBox(10, logoLabel, versionLabel, new Spacer(), githubLink);
|
||||
logoBox.getStyleClass().add("logo");
|
||||
logoBox.setAlignment(Pos.CENTER_LEFT);
|
||||
logoBox.setMinWidth(SIDEBAR_WIDTH);
|
||||
logoBox.setPrefWidth(SIDEBAR_WIDTH);
|
||||
logoBox.setMaxWidth(SIDEBAR_WIDTH);
|
||||
|
||||
var titleLabel = new Label();
|
||||
titleLabel.getStyleClass().addAll("page-title", TITLE_4);
|
||||
titleLabel.textProperty().bind(model.titleProperty());
|
||||
|
||||
var searchField = new CustomTextField();
|
||||
searchField.setLeft(new FontIcon(Material2MZ.SEARCH));
|
||||
searchField.setPromptText("Search");
|
||||
model.searchTextProperty().bind(searchField.textProperty());
|
||||
|
||||
DefaultEventBus.getInstance().subscribe(HotkeyEvent.class, e -> {
|
||||
if (e.getKeys().getControl() == ModifierValue.DOWN && e.getKeys().getCode() == KeyCode.F) {
|
||||
searchField.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
// this dummy anchor prevents popover from inheriting
|
||||
// unwanted styles from the owner node
|
||||
var popoverAnchor = new Region();
|
||||
|
||||
var quickConfigBtn = new FontIcon(Material2OutlinedMZ.STYLE);
|
||||
quickConfigBtn.mouseTransparentProperty().bind(model.themeChangeToggleProperty().not());
|
||||
quickConfigBtn.opacityProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> model.themeChangeToggleProperty().get() ? 1.0 : 0.5, model.themeChangeToggleProperty()
|
||||
));
|
||||
quickConfigBtn.setOnMouseClicked(e -> {
|
||||
if (quickConfigActionHandler != null) { quickConfigActionHandler.accept(popoverAnchor); }
|
||||
});
|
||||
|
||||
var sourceCodeBtn = new FontIcon(ICON_CODE);
|
||||
sourceCodeBtn.mouseTransparentProperty().bind(model.sourceCodeToggleProperty().not());
|
||||
sourceCodeBtn.opacityProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> model.sourceCodeToggleProperty().get() ? 1.0 : 0.5, model.sourceCodeToggleProperty()
|
||||
));
|
||||
sourceCodeBtn.setOnMouseClicked(e -> model.nextSubLayer());
|
||||
|
||||
// ~
|
||||
|
||||
model.currentSubLayerProperty().addListener((obs, old, val) -> {
|
||||
switch (val) {
|
||||
case PAGE -> sourceCodeBtn.setIconCode(ICON_CODE);
|
||||
case SOURCE_CODE -> sourceCodeBtn.setIconCode(ICON_SAMPLE);
|
||||
}
|
||||
});
|
||||
|
||||
setId("header-bar");
|
||||
setMinHeight(HEADER_HEIGHT);
|
||||
setPrefHeight(HEADER_HEIGHT);
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
getChildren().setAll(
|
||||
logoBox,
|
||||
titleLabel,
|
||||
new Spacer(),
|
||||
searchField,
|
||||
popoverAnchor,
|
||||
quickConfigBtn,
|
||||
sourceCodeBtn
|
||||
);
|
||||
|
||||
if (IS_DEV_MODE) {
|
||||
var devModeLabel = new Label("app is running in development mode");
|
||||
devModeLabel.getStyleClass().addAll(TEXT_SMALL, "dev-mode-indicator");
|
||||
getChildren().add(2, devModeLabel);
|
||||
}
|
||||
}
|
||||
|
||||
void setQuickConfigActionHandler(Consumer<Node> handler) {
|
||||
this.quickConfigActionHandler = handler;
|
||||
}
|
||||
}
|
@ -1,67 +1,168 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.layout;
|
||||
|
||||
import atlantafx.base.controls.Popover;
|
||||
import atlantafx.sampler.event.DefaultEventBus;
|
||||
import atlantafx.sampler.event.ThemeEvent;
|
||||
import atlantafx.sampler.layout.MainModel.SubLayer;
|
||||
import atlantafx.sampler.page.CodeViewer;
|
||||
import atlantafx.sampler.page.Page;
|
||||
import atlantafx.sampler.page.QuickConfigMenu;
|
||||
import atlantafx.sampler.page.components.OverviewPage;
|
||||
import atlantafx.sampler.theme.ThemeManager;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import static atlantafx.base.controls.Popover.ArrowLocation.TOP_CENTER;
|
||||
import static javafx.scene.layout.Priority.ALWAYS;
|
||||
|
||||
class MainLayer extends BorderPane {
|
||||
|
||||
static final int SIDEBAR_WIDTH = 220;
|
||||
static final int PAGE_TRANSITION_DURATION = 500; // ms
|
||||
|
||||
private final MainModel model = new MainModel();
|
||||
private final HeaderBar headerBar = new HeaderBar(model);
|
||||
private final Sidebar sidebar = new Sidebar(model);
|
||||
private final StackPane subLayerPane = new StackPane();
|
||||
|
||||
private Popover quickConfigPopover;
|
||||
private CodeViewer codeViewer;
|
||||
private StackPane codeViewerWrapper;
|
||||
|
||||
public MainLayer() {
|
||||
super();
|
||||
|
||||
createView();
|
||||
initListeners();
|
||||
|
||||
model.navigate(OverviewPage.class);
|
||||
// keyboard navigation won't work without focus
|
||||
Platform.runLater(sidebar::begForFocus);
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
var sidebar = new Sidebar();
|
||||
sidebar.setMinWidth(200);
|
||||
sidebar.setMinWidth(SIDEBAR_WIDTH);
|
||||
|
||||
final var pageContainer = new StackPane();
|
||||
HBox.setHgrow(pageContainer, ALWAYS);
|
||||
codeViewer = new CodeViewer();
|
||||
|
||||
sidebar.setOnSelect(pageClass -> {
|
||||
try {
|
||||
final Page prevPage = (!pageContainer.getChildren().isEmpty() && pageContainer.getChildren().get(0) instanceof Page page) ? page : null;
|
||||
final Page nextPage = pageClass.getDeclaredConstructor().newInstance();
|
||||
codeViewerWrapper = new StackPane();
|
||||
codeViewerWrapper.getStyleClass().add("source-code");
|
||||
codeViewerWrapper.getChildren().setAll(codeViewer);
|
||||
|
||||
// startup, no animation
|
||||
if (getScene() == null) {
|
||||
pageContainer.getChildren().add(nextPage.getView());
|
||||
return;
|
||||
}
|
||||
subLayerPane.getChildren().setAll(codeViewerWrapper);
|
||||
HBox.setHgrow(subLayerPane, ALWAYS);
|
||||
|
||||
Objects.requireNonNull(prevPage);
|
||||
// ~
|
||||
|
||||
// reset previous page, e.g. to free resources
|
||||
prevPage.reset();
|
||||
setId("main");
|
||||
setTop(headerBar);
|
||||
setLeft(sidebar);
|
||||
setCenter(subLayerPane);
|
||||
}
|
||||
|
||||
// animate switching between pages
|
||||
pageContainer.getChildren().add(nextPage.getView());
|
||||
FadeTransition transition = new FadeTransition(Duration.millis(300), nextPage.getView());
|
||||
transition.setFromValue(0.0);
|
||||
transition.setToValue(1.0);
|
||||
transition.setOnFinished(t -> pageContainer.getChildren().remove(prevPage.getView()));
|
||||
transition.play();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
private void initListeners() {
|
||||
headerBar.setQuickConfigActionHandler(this::showThemeConfigPopover);
|
||||
|
||||
model.selectedPageProperty().addListener((obs, old, val) -> {
|
||||
if (val != null) { loadPage(val); }
|
||||
});
|
||||
|
||||
model.currentSubLayerProperty().addListener((obs, old, val) -> {
|
||||
switch (val) {
|
||||
case PAGE -> hideSourceCode();
|
||||
case SOURCE_CODE -> showSourceCode();
|
||||
}
|
||||
});
|
||||
|
||||
// ~
|
||||
setLeft(sidebar);
|
||||
setCenter(pageContainer);
|
||||
// update code view color theme on app theme change
|
||||
DefaultEventBus.getInstance().subscribe(ThemeEvent.class, e -> {
|
||||
if (ThemeManager.getInstance().getTheme() != null && model.currentSubLayerProperty().get() == SubLayer.SOURCE_CODE) {
|
||||
showSourceCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sidebar.select(OverviewPage.class);
|
||||
Platform.runLater(sidebar::requestFocus);
|
||||
private void loadPage(Class<? extends Page> pageClass) {
|
||||
try {
|
||||
final Page prevPage = (Page) subLayerPane.getChildren().stream()
|
||||
.filter(c -> c instanceof Page)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
final Page nextPage = pageClass.getDeclaredConstructor().newInstance();
|
||||
|
||||
model.setPageData(
|
||||
nextPage.getName(),
|
||||
nextPage.canChangeThemeSettings(),
|
||||
nextPage.canDisplaySourceCode()
|
||||
);
|
||||
|
||||
// startup, no prev page, no animation
|
||||
if (getScene() == null) {
|
||||
subLayerPane.getChildren().add(nextPage.getView());
|
||||
return;
|
||||
}
|
||||
|
||||
Objects.requireNonNull(prevPage);
|
||||
|
||||
// reset previous page, e.g. to free resources
|
||||
prevPage.reset();
|
||||
|
||||
// animate switching between pages
|
||||
subLayerPane.getChildren().add(nextPage.getView());
|
||||
var transition = new FadeTransition(Duration.millis(PAGE_TRANSITION_DURATION), nextPage.getView());
|
||||
transition.setFromValue(0.0);
|
||||
transition.setToValue(1.0);
|
||||
transition.setOnFinished(t -> {
|
||||
subLayerPane.getChildren().remove(prevPage.getView());
|
||||
if (nextPage instanceof Pane nextPane) { nextPane.toFront(); }
|
||||
});
|
||||
transition.play();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void showThemeConfigPopover(Node source) {
|
||||
if (quickConfigPopover == null) {
|
||||
var content = new QuickConfigMenu();
|
||||
content.setExitHandler(() -> quickConfigPopover.hide());
|
||||
|
||||
quickConfigPopover = new Popover(content);
|
||||
quickConfigPopover.setHeaderAlwaysVisible(false);
|
||||
quickConfigPopover.setDetachable(false);
|
||||
quickConfigPopover.setArrowLocation(TOP_CENTER);
|
||||
quickConfigPopover.setOnShowing(e -> content.update());
|
||||
}
|
||||
|
||||
quickConfigPopover.show(source);
|
||||
}
|
||||
|
||||
private void showSourceCode() {
|
||||
var sourceClass = Objects.requireNonNull(model.selectedPageProperty().get());
|
||||
var sourceFileName = sourceClass.getSimpleName() + ".java";
|
||||
try (var stream = sourceClass.getResourceAsStream(sourceFileName)) {
|
||||
Objects.requireNonNull(stream, "Missing source file '" + sourceFileName + "';");
|
||||
|
||||
// set syntax highlight theme according to JavaFX theme
|
||||
ThemeManager tm = ThemeManager.getInstance();
|
||||
codeViewer.setContent(stream, tm.getMatchingSourceCodeHighlightTheme(tm.getTheme()));
|
||||
codeViewerWrapper.toFront();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideSourceCode() {
|
||||
codeViewerWrapper.toBack();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.layout;
|
||||
|
||||
import atlantafx.sampler.page.Page;
|
||||
import javafx.beans.property.*;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static atlantafx.sampler.layout.MainModel.SubLayer.PAGE;
|
||||
import static atlantafx.sampler.layout.MainModel.SubLayer.SOURCE_CODE;
|
||||
|
||||
public class MainModel {
|
||||
|
||||
public enum SubLayer {
|
||||
PAGE,
|
||||
SOURCE_CODE
|
||||
}
|
||||
|
||||
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper();
|
||||
private final StringProperty searchText = new SimpleStringProperty();
|
||||
private final ReadOnlyObjectWrapper<Class<? extends Page>> selectedPage = new ReadOnlyObjectWrapper<>();
|
||||
private final ReadOnlyBooleanWrapper themeChangeToggle = new ReadOnlyBooleanWrapper();
|
||||
private final ReadOnlyBooleanWrapper sourceCodeToggle = new ReadOnlyBooleanWrapper();
|
||||
private final ReadOnlyObjectWrapper<SubLayer> currentSubLayer = new ReadOnlyObjectWrapper<>(PAGE);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Properties //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public StringProperty searchTextProperty() {
|
||||
return searchText;
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty titleProperty() {
|
||||
return title.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty themeChangeToggleProperty() {
|
||||
return themeChangeToggle.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty sourceCodeToggleProperty() {
|
||||
return sourceCodeToggle.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Class<? extends Page>> selectedPageProperty() {
|
||||
return selectedPage.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<SubLayer> currentSubLayerProperty() {
|
||||
return currentSubLayer.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Commands //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void setPageData(String text, boolean canChangeTheme, boolean canDisplaySource) {
|
||||
title.set(Objects.requireNonNull(text));
|
||||
themeChangeToggle.set(canChangeTheme);
|
||||
sourceCodeToggle.set(canDisplaySource);
|
||||
}
|
||||
|
||||
public void navigate(Class<? extends Page> page) {
|
||||
selectedPage.set(Objects.requireNonNull(page));
|
||||
currentSubLayer.set(PAGE);
|
||||
}
|
||||
|
||||
public void nextSubLayer() {
|
||||
var old = currentSubLayer.get();
|
||||
currentSubLayer.set(old == PAGE ? SOURCE_CODE : PAGE);
|
||||
}
|
||||
}
|
@ -11,193 +11,243 @@ import atlantafx.sampler.page.showcase.filemanager.FileManagerPage;
|
||||
import atlantafx.sampler.page.showcase.musicplayer.MusicPlayerPage;
|
||||
import atlantafx.sampler.util.Containers;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED;
|
||||
import static javafx.scene.layout.Priority.ALWAYS;
|
||||
|
||||
class Sidebar extends VBox {
|
||||
class Sidebar extends StackPane {
|
||||
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
private static final PseudoClass FILTERED = PseudoClass.getPseudoClass("filtered");
|
||||
private static final Predicate<Region> PREDICATE_ANY = region -> true;
|
||||
|
||||
private final FilteredList<Region> navigationMenu = navigationMenu();
|
||||
private final ReadOnlyObjectWrapper<NavLink> selectedLink = new ReadOnlyObjectWrapper<>();
|
||||
private Consumer<Class<? extends Page>> navigationHandler;
|
||||
private final MainModel model;
|
||||
private final NavMenu navMenu;
|
||||
private ScrollPane navScroll;
|
||||
|
||||
public Sidebar() {
|
||||
public Sidebar(MainModel model) {
|
||||
super();
|
||||
setId("sidebar");
|
||||
|
||||
this.model = model;
|
||||
this.navMenu = new NavMenu(model);
|
||||
|
||||
createView();
|
||||
|
||||
selectedLink.addListener((obs, old, val) -> {
|
||||
if (navigationHandler != null) {
|
||||
navigationHandler.accept(val != null ? val.getPageClass() : null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void select(Class<? extends Page> pageClass) {
|
||||
navigationMenu.stream()
|
||||
.filter(region -> region instanceof NavLink link && pageClass.equals(link.getPageClass()))
|
||||
.findFirst()
|
||||
.ifPresent(link -> navigate((NavLink) link));
|
||||
}
|
||||
|
||||
public void setOnSelect(Consumer<Class<? extends Page>> c) {
|
||||
navigationHandler = c;
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
var placeholder = new Label("No content");
|
||||
placeholder.getStyleClass().add(Styles.TITLE_4);
|
||||
|
||||
var navContainer = new VBox();
|
||||
navContainer.getStyleClass().add("nav-menu");
|
||||
Bindings.bindContent(navContainer.getChildren(), navigationMenu);
|
||||
Bindings.bindContent(navContainer.getChildren(), navMenu.getContent());
|
||||
|
||||
var navScroll = new ScrollPane(navContainer);
|
||||
Containers.setScrollConstraints(navScroll,
|
||||
ScrollPane.ScrollBarPolicy.AS_NEEDED, true,
|
||||
ScrollPane.ScrollBarPolicy.AS_NEEDED, true
|
||||
);
|
||||
navScroll = new ScrollPane(navContainer);
|
||||
Containers.setScrollConstraints(navScroll, AS_NEEDED, true, AS_NEEDED, true);
|
||||
VBox.setVgrow(navScroll, ALWAYS);
|
||||
|
||||
// == SEARCH FORM ==
|
||||
model.searchTextProperty().addListener((obs, old, val) -> {
|
||||
var empty = val == null || val.isBlank();
|
||||
pseudoClassStateChanged(FILTERED, !empty);
|
||||
navMenu.setPredicate(empty ? PREDICATE_ANY : region -> region instanceof NavLink link && link.matches(val));
|
||||
});
|
||||
|
||||
var searchField = new TextField();
|
||||
searchField.setPromptText("Search");
|
||||
HBox.setHgrow(searchField, ALWAYS);
|
||||
searchField.textProperty().addListener((obs, old, val) -> {
|
||||
if (val == null || val.isBlank()) {
|
||||
navigationMenu.setPredicate(c -> true);
|
||||
return;
|
||||
model.selectedPageProperty().addListener((obs, old, val) -> {
|
||||
navMenu.findLink(old).ifPresent(link -> link.pseudoClassStateChanged(SELECTED, false));
|
||||
navMenu.findLink(val).ifPresent(link -> link.pseudoClassStateChanged(SELECTED, true));
|
||||
});
|
||||
|
||||
navScroll.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
|
||||
var offset = 1 / (navContainer.getHeight() - navScroll.getViewportBounds().getHeight());
|
||||
if (e.getCode() == KeyCode.UP) {
|
||||
navMenu.getPrevious().ifPresentOrElse(link -> {
|
||||
navScroll.setVvalue(link.getLayoutY() * offset / 2);
|
||||
model.navigate(link.getPageClass());
|
||||
}, () -> navScroll.setVvalue(0));
|
||||
e.consume();
|
||||
}
|
||||
if (e.getCode() == KeyCode.DOWN) {
|
||||
navMenu.getNext().ifPresentOrElse(link -> {
|
||||
navScroll.setVvalue(link.getLayoutY() * offset / 2);
|
||||
model.navigate(link.getPageClass());
|
||||
}, () -> navScroll.setVvalue(1.0));
|
||||
e.consume();
|
||||
}
|
||||
navigationMenu.setPredicate(c -> c instanceof NavLink link && link.matches(val));
|
||||
});
|
||||
|
||||
var searchForm = new HBox(searchField);
|
||||
searchForm.setId("search-form");
|
||||
searchForm.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
// ~
|
||||
|
||||
getChildren().addAll(searchForm, navScroll);
|
||||
}
|
||||
|
||||
private Label caption(String text) {
|
||||
var label = new Label(text);
|
||||
label.getStyleClass().add("caption");
|
||||
label.setMaxWidth(Double.MAX_VALUE);
|
||||
return label;
|
||||
}
|
||||
|
||||
private FilteredList<Region> navigationMenu() {
|
||||
return new FilteredList<>(FXCollections.observableArrayList(
|
||||
caption("GENERAL"),
|
||||
navLink(ThemePage.NAME, ThemePage.class),
|
||||
navLink(TypographyPage.NAME, TypographyPage.class),
|
||||
new Separator(),
|
||||
caption("COMPONENTS"),
|
||||
navLink(OverviewPage.NAME, OverviewPage.class),
|
||||
navLink(InputGroupPage.NAME, InputGroupPage.class),
|
||||
new Spacer(10, Orientation.VERTICAL),
|
||||
navLink(AccordionPage.NAME, AccordionPage.class),
|
||||
navLink(BreadcrumbsPage.NAME, BreadcrumbsPage.class),
|
||||
navLink(ButtonPage.NAME, ButtonPage.class),
|
||||
navLink(ChartPage.NAME, ChartPage.class),
|
||||
navLink(CheckBoxPage.NAME, CheckBoxPage.class),
|
||||
navLink(ColorPickerPage.NAME, ColorPickerPage.class),
|
||||
navLink(ComboBoxPage.NAME, ComboBoxPage.class, "ChoiceBox"),
|
||||
navLink(CustomTextFieldPage.NAME, CustomTextFieldPage.class),
|
||||
navLink(DatePickerPage.NAME, DatePickerPage.class),
|
||||
navLink(DialogPage.NAME, DialogPage.class),
|
||||
navLink(HTMLEditorPage.NAME, HTMLEditorPage.class),
|
||||
navLink(ListPage.NAME, ListPage.class),
|
||||
navLink(MenuPage.NAME, MenuPage.class),
|
||||
navLink(MenuButtonPage.NAME, MenuButtonPage.class, "SplitMenuButton"),
|
||||
navLink(PaginationPage.NAME, PaginationPage.class),
|
||||
navLink(PopoverPage.NAME, PopoverPage.class),
|
||||
navLink(ProgressPage.NAME, ProgressPage.class),
|
||||
navLink(RadioButtonPage.NAME, RadioButtonPage.class),
|
||||
navLink(ScrollPanePage.NAME, ScrollPanePage.class),
|
||||
navLink(SeparatorPage.NAME, SeparatorPage.class),
|
||||
navLink(SliderPage.NAME, SliderPage.class),
|
||||
navLink(SpinnerPage.NAME, SpinnerPage.class),
|
||||
navLink(SplitPanePage.NAME, SplitPanePage.class),
|
||||
navLink(TablePage.NAME, TablePage.class),
|
||||
navLink(TabPanePage.NAME, TabPanePage.class),
|
||||
navLink(TextAreaPage.NAME, TextAreaPage.class),
|
||||
navLink(TextFieldPage.NAME, TextFieldPage.class, "PasswordField"),
|
||||
navLink(TitledPanePage.NAME, TitledPanePage.class),
|
||||
navLink(ToggleButtonPage.NAME, ToggleButtonPage.class),
|
||||
navLink(ToggleSwitchPage.NAME, ToggleSwitchPage.class),
|
||||
navLink(ToolBarPage.NAME, ToolBarPage.class),
|
||||
navLink(TooltipPage.NAME, TooltipPage.class),
|
||||
navLink(TreePage.NAME, TreePage.class),
|
||||
navLink(TreeTablePage.NAME, TreeTablePage.class),
|
||||
caption("SHOWCASE"),
|
||||
navLink(FileManagerPage.NAME, FileManagerPage.class),
|
||||
navLink(MusicPlayerPage.NAME, MusicPlayerPage.class)
|
||||
));
|
||||
}
|
||||
|
||||
private NavLink navLink(String text, Class<? extends Page> pageClass, String... keywords) {
|
||||
return navLink(text, pageClass, false, keywords);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private NavLink navLink(String text, Class<? extends Page> pageClass, boolean isNew, String... keywords) {
|
||||
var link = new NavLink(text, pageClass, isNew);
|
||||
|
||||
if (keywords != null && keywords.length > 0) {
|
||||
link.getSearchKeywords().addAll(Arrays.asList(keywords));
|
||||
}
|
||||
|
||||
link.setOnMouseClicked(e -> {
|
||||
if (e.getSource() instanceof NavLink dest) { navigate(dest); }
|
||||
navMenu.getContent().addListener((ListChangeListener<Region>) c -> {
|
||||
if (navMenu.getContent().isEmpty()) {
|
||||
placeholder.toFront();
|
||||
} else {
|
||||
placeholder.toBack();
|
||||
}
|
||||
});
|
||||
|
||||
return link;
|
||||
setId("sidebar");
|
||||
getChildren().addAll(placeholder, navScroll);
|
||||
}
|
||||
|
||||
private void navigate(NavLink link) {
|
||||
if (selectedLink.get() != null) { selectedLink.get().pseudoClassStateChanged(SELECTED, false); }
|
||||
link.pseudoClassStateChanged(SELECTED, true);
|
||||
selectedLink.set(link);
|
||||
void begForFocus() {
|
||||
navScroll.requestFocus();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static class NavMenu {
|
||||
|
||||
private final MainModel model;
|
||||
private final FilteredList<Region> content;
|
||||
private final Map<Class<? extends Page>, NavLink> registry = new HashMap<>();
|
||||
|
||||
public NavMenu(MainModel model) {
|
||||
var links = create();
|
||||
|
||||
this.model = model;
|
||||
this.content = new FilteredList<>(links);
|
||||
links.forEach(c -> {
|
||||
if (c instanceof NavLink link) {
|
||||
registry.put(link.getPageClass(), link);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public FilteredList<Region> getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setPredicate(Predicate<Region> predicate) {
|
||||
content.setPredicate(predicate);
|
||||
}
|
||||
|
||||
public Optional<NavLink> findLink(Class<? extends Page> pageClass) {
|
||||
if (pageClass == null) { return Optional.empty(); }
|
||||
return Optional.ofNullable(registry.get(pageClass));
|
||||
}
|
||||
|
||||
public Optional<NavLink> getPrevious() {
|
||||
var current = content.indexOf(registry.get(model.selectedPageProperty().get()));
|
||||
if (!(current > 0)) { return Optional.empty(); }
|
||||
|
||||
for (int i = current - 1; i >= 0; i--) {
|
||||
var r = content.get(i);
|
||||
if (r instanceof NavLink link) { return Optional.of(link); }
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<NavLink> getNext() {
|
||||
var current = content.indexOf(registry.get(model.selectedPageProperty().get()));
|
||||
if (!(current >= 0 && current < content.size() - 1)) { return Optional.empty(); } // has next
|
||||
|
||||
for (int i = current + 1; i < content.size(); i++) {
|
||||
var r = content.get(i);
|
||||
if (r instanceof NavLink link) { return Optional.of(link); }
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private ObservableList<Region> create() {
|
||||
return FXCollections.observableArrayList(
|
||||
caption("GENERAL"),
|
||||
navLink(ThemePage.NAME, ThemePage.class),
|
||||
navLink(TypographyPage.NAME, TypographyPage.class),
|
||||
caption("COMPONENTS"),
|
||||
navLink(OverviewPage.NAME, OverviewPage.class),
|
||||
navLink(InputGroupPage.NAME, InputGroupPage.class),
|
||||
new Spacer(10, Orientation.VERTICAL),
|
||||
navLink(AccordionPage.NAME, AccordionPage.class),
|
||||
navLink(BreadcrumbsPage.NAME, BreadcrumbsPage.class),
|
||||
navLink(ButtonPage.NAME, ButtonPage.class),
|
||||
navLink(ChartPage.NAME, ChartPage.class),
|
||||
navLink(CheckBoxPage.NAME, CheckBoxPage.class),
|
||||
navLink(ColorPickerPage.NAME, ColorPickerPage.class),
|
||||
navLink(ComboBoxPage.NAME, ComboBoxPage.class, "ChoiceBox"),
|
||||
navLink(CustomTextFieldPage.NAME, CustomTextFieldPage.class),
|
||||
navLink(DatePickerPage.NAME, DatePickerPage.class),
|
||||
navLink(DialogPage.NAME, DialogPage.class),
|
||||
navLink(HTMLEditorPage.NAME, HTMLEditorPage.class),
|
||||
navLink(ListPage.NAME, ListPage.class),
|
||||
navLink(MenuPage.NAME, MenuPage.class),
|
||||
navLink(MenuButtonPage.NAME, MenuButtonPage.class, "SplitMenuButton"),
|
||||
navLink(PaginationPage.NAME, PaginationPage.class),
|
||||
navLink(PopoverPage.NAME, PopoverPage.class),
|
||||
navLink(ProgressPage.NAME, ProgressPage.class),
|
||||
navLink(RadioButtonPage.NAME, RadioButtonPage.class),
|
||||
navLink(ScrollPanePage.NAME, ScrollPanePage.class),
|
||||
navLink(SeparatorPage.NAME, SeparatorPage.class),
|
||||
navLink(SliderPage.NAME, SliderPage.class),
|
||||
navLink(SpinnerPage.NAME, SpinnerPage.class),
|
||||
navLink(SplitPanePage.NAME, SplitPanePage.class),
|
||||
navLink(TablePage.NAME, TablePage.class),
|
||||
navLink(TabPanePage.NAME, TabPanePage.class),
|
||||
navLink(TextAreaPage.NAME, TextAreaPage.class),
|
||||
navLink(TextFieldPage.NAME, TextFieldPage.class, "PasswordField"),
|
||||
navLink(TitledPanePage.NAME, TitledPanePage.class),
|
||||
navLink(ToggleButtonPage.NAME, ToggleButtonPage.class),
|
||||
navLink(ToggleSwitchPage.NAME, ToggleSwitchPage.class),
|
||||
navLink(ToolBarPage.NAME, ToolBarPage.class),
|
||||
navLink(TooltipPage.NAME, TooltipPage.class),
|
||||
navLink(TreePage.NAME, TreePage.class),
|
||||
navLink(TreeTablePage.NAME, TreeTablePage.class),
|
||||
caption("SHOWCASE"),
|
||||
navLink(FileManagerPage.NAME, FileManagerPage.class),
|
||||
navLink(MusicPlayerPage.NAME, MusicPlayerPage.class)
|
||||
);
|
||||
}
|
||||
|
||||
private Label caption(String text) {
|
||||
var label = new Label(text);
|
||||
label.getStyleClass().add("caption");
|
||||
label.setMaxWidth(Double.MAX_VALUE);
|
||||
return label;
|
||||
}
|
||||
|
||||
private NavLink navLink(String text, Class<? extends Page> pageClass, String... keywords) {
|
||||
var link = new NavLink(text, pageClass);
|
||||
|
||||
if (keywords != null && keywords.length > 0) {
|
||||
link.getSearchKeywords().addAll(Arrays.asList(keywords));
|
||||
}
|
||||
|
||||
link.setOnMouseClicked(e -> {
|
||||
if (e.getSource() instanceof NavLink target) {
|
||||
model.navigate(target.getPageClass());
|
||||
}
|
||||
});
|
||||
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NavLink extends Label {
|
||||
|
||||
private final Class<? extends Page> pageClass;
|
||||
private final List<String> searchKeywords = new ArrayList<>();
|
||||
|
||||
public NavLink(String text, Class<? extends Page> pageClass, boolean isNew) {
|
||||
public NavLink(String text, Class<? extends Page> pageClass) {
|
||||
super(Objects.requireNonNull(text));
|
||||
this.pageClass = Objects.requireNonNull(pageClass);
|
||||
|
||||
getStyleClass().add("nav-link");
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
setContentDisplay(ContentDisplay.RIGHT);
|
||||
|
||||
if (isNew) {
|
||||
var tag = new Label("new");
|
||||
tag.getStyleClass().addAll("tag", Styles.TEXT_SMALL);
|
||||
setGraphic(tag);
|
||||
}
|
||||
}
|
||||
|
||||
public Class<? extends Page> getPageClass() {
|
||||
|
@ -1,55 +1,32 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page;
|
||||
|
||||
import atlantafx.base.controls.Popover;
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import atlantafx.base.theme.Styles;
|
||||
import atlantafx.sampler.event.DefaultEventBus;
|
||||
import atlantafx.sampler.event.ThemeEvent;
|
||||
import atlantafx.sampler.layout.Overlay;
|
||||
import atlantafx.sampler.theme.ThemeManager;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import net.datafaker.Faker;
|
||||
import org.kordamp.ikonli.Ikon;
|
||||
import org.kordamp.ikonli.feather.Feather;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
import org.kordamp.ikonli.material2.Material2OutlinedMZ;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static atlantafx.base.controls.Popover.ArrowLocation.TOP_CENTER;
|
||||
import static atlantafx.base.theme.Styles.*;
|
||||
import static atlantafx.sampler.Launcher.IS_DEV_MODE;
|
||||
import static atlantafx.sampler.util.Containers.setScrollConstraints;
|
||||
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED;
|
||||
|
||||
public abstract class AbstractPage extends BorderPane implements Page {
|
||||
|
||||
protected static final int HEADER_HEIGHT = 50;
|
||||
|
||||
protected static final Faker FAKER = new Faker();
|
||||
protected static final Random RANDOM = new Random();
|
||||
protected static final EventHandler<ActionEvent> PRINT_SOURCE = System.out::println;
|
||||
private static final Ikon ICON_CODE = Feather.CODE;
|
||||
private static final Ikon ICON_SAMPLE = Feather.LAYOUT;
|
||||
|
||||
protected Button quickConfigBtn;
|
||||
protected Popover quickConfigPopover;
|
||||
protected Button sourceCodeToggleBtn;
|
||||
protected StackPane codeViewerWrapper;
|
||||
protected CodeViewer codeViewer;
|
||||
protected VBox userContent;
|
||||
protected Overlay overlay;
|
||||
protected boolean isRendered = false;
|
||||
@ -59,50 +36,9 @@ public abstract class AbstractPage extends BorderPane implements Page {
|
||||
|
||||
getStyleClass().add("page");
|
||||
createPageLayout();
|
||||
|
||||
// update code view color theme on app theme change
|
||||
DefaultEventBus.getInstance().subscribe(ThemeEvent.class, e -> {
|
||||
if (ThemeManager.getInstance().getTheme() != null && isCodeViewActive()) {
|
||||
loadSourceCodeAndMoveToFront();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void createPageLayout() {
|
||||
// == header ==
|
||||
|
||||
var titleLabel = new Label(getName());
|
||||
titleLabel.getStyleClass().addAll(Styles.TITLE_4);
|
||||
|
||||
codeViewer = new CodeViewer();
|
||||
|
||||
codeViewerWrapper = new StackPane();
|
||||
codeViewerWrapper.getStyleClass().add("wrapper");
|
||||
codeViewerWrapper.getChildren().setAll(codeViewer);
|
||||
|
||||
quickConfigBtn = new Button("", new FontIcon(Material2OutlinedMZ.STYLE));
|
||||
quickConfigBtn.getStyleClass().addAll(BUTTON_ICON, FLAT);
|
||||
quickConfigBtn.setTooltip(new Tooltip("Change theme"));
|
||||
quickConfigBtn.setOnAction(e -> showThemeConfigPopover());
|
||||
|
||||
sourceCodeToggleBtn = new Button("", new FontIcon(ICON_CODE));
|
||||
sourceCodeToggleBtn.getStyleClass().addAll(BUTTON_ICON, FLAT);
|
||||
sourceCodeToggleBtn.setTooltip(new Tooltip("Source code"));
|
||||
sourceCodeToggleBtn.setOnAction(e -> toggleSourceCode());
|
||||
|
||||
var header = new HBox(30);
|
||||
header.getStyleClass().add("header");
|
||||
header.setMinHeight(HEADER_HEIGHT);
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.getChildren().setAll(titleLabel, new Spacer(), quickConfigBtn, sourceCodeToggleBtn);
|
||||
if (IS_DEV_MODE) {
|
||||
var devModeLabel = new Label("App is running in development mode");
|
||||
devModeLabel.getStyleClass().addAll(TEXT_SMALL, "dev-mode-indicator");
|
||||
header.getChildren().add(1, devModeLabel);
|
||||
}
|
||||
|
||||
// == user content ==
|
||||
|
||||
userContent = new VBox();
|
||||
userContent.getStyleClass().add("user-content");
|
||||
|
||||
@ -111,20 +47,10 @@ public abstract class AbstractPage extends BorderPane implements Page {
|
||||
userContentWrapper.getChildren().setAll(userContent);
|
||||
|
||||
var scrollPane = new ScrollPane(userContentWrapper);
|
||||
setScrollConstraints(scrollPane,
|
||||
ScrollPane.ScrollBarPolicy.AS_NEEDED, true,
|
||||
ScrollPane.ScrollBarPolicy.AS_NEEDED, true
|
||||
);
|
||||
setScrollConstraints(scrollPane, AS_NEEDED, true, AS_NEEDED, true);
|
||||
scrollPane.setMaxHeight(10_000);
|
||||
|
||||
// == layout ==
|
||||
|
||||
var stackPane = new StackPane();
|
||||
stackPane.getStyleClass().add("stack");
|
||||
stackPane.getChildren().addAll(codeViewerWrapper, scrollPane);
|
||||
|
||||
setTop(header);
|
||||
setCenter(stackPane);
|
||||
setCenter(scrollPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -132,6 +58,16 @@ public abstract class AbstractPage extends BorderPane implements Page {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDisplaySourceCode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canChangeThemeSettings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() { }
|
||||
|
||||
@ -150,65 +86,9 @@ public abstract class AbstractPage extends BorderPane implements Page {
|
||||
}
|
||||
|
||||
protected Overlay lookupOverlay() {
|
||||
return getScene() != null && getScene().lookup("." + Overlay.STYLE_CLASS) instanceof Overlay overlay ?
|
||||
overlay : null;
|
||||
return getScene() != null && getScene().lookup("." + Overlay.STYLE_CLASS) instanceof Overlay overlay ? overlay : null;
|
||||
}
|
||||
|
||||
protected void showThemeConfigPopover() {
|
||||
if (quickConfigPopover == null) {
|
||||
var content = new QuickConfigMenu();
|
||||
content.setExitHandler(() -> quickConfigPopover.hide());
|
||||
|
||||
quickConfigPopover = new Popover(content);
|
||||
quickConfigPopover.setHeaderAlwaysVisible(false);
|
||||
quickConfigPopover.setDetachable(false);
|
||||
quickConfigPopover.setArrowLocation(TOP_CENTER);
|
||||
quickConfigPopover.setOnShowing(e -> content.update());
|
||||
}
|
||||
|
||||
quickConfigPopover.show(quickConfigBtn);
|
||||
}
|
||||
|
||||
protected void toggleSourceCode() {
|
||||
if (isCodeViewActive()) {
|
||||
codeViewerWrapper.toBack();
|
||||
((FontIcon) sourceCodeToggleBtn.getGraphic()).setIconCode(ICON_CODE);
|
||||
return;
|
||||
}
|
||||
|
||||
loadSourceCodeAndMoveToFront();
|
||||
}
|
||||
|
||||
protected void loadSourceCodeAndMoveToFront() {
|
||||
var sourceFileName = getClass().getSimpleName() + ".java";
|
||||
try (var stream = getClass().getResourceAsStream(sourceFileName)) {
|
||||
Objects.requireNonNull(stream, "Missing source file '" + sourceFileName + "';");
|
||||
|
||||
// set syntax highlight theme according to JavaFX theme
|
||||
ThemeManager tm = ThemeManager.getInstance();
|
||||
codeViewer.setContent(stream, tm.getMatchingSourceCodeHighlightTheme(tm.getTheme()));
|
||||
|
||||
var graphic = (FontIcon) sourceCodeToggleBtn.getGraphic();
|
||||
graphic.setIconCode(ICON_SAMPLE);
|
||||
|
||||
codeViewerWrapper.toFront();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected final boolean isSampleViewActive() {
|
||||
var graphic = (FontIcon) sourceCodeToggleBtn.getGraphic();
|
||||
return graphic.getIconCode() == ICON_CODE;
|
||||
}
|
||||
|
||||
protected final boolean isCodeViewActive() {
|
||||
var graphic = (FontIcon) sourceCodeToggleBtn.getGraphic();
|
||||
return graphic.getIconCode() == ICON_SAMPLE;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Helpers //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected <T> List<T> generate(Supplier<T> supplier, int count) {
|
||||
|
@ -9,5 +9,9 @@ public interface Page {
|
||||
|
||||
Parent getView();
|
||||
|
||||
boolean canDisplaySourceCode();
|
||||
|
||||
boolean canChangeThemeSettings();
|
||||
|
||||
void reset();
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import atlantafx.sampler.event.ThemeEvent;
|
||||
import atlantafx.sampler.page.AbstractPage;
|
||||
import atlantafx.sampler.theme.SamplerTheme;
|
||||
import atlantafx.sampler.theme.ThemeManager;
|
||||
import atlantafx.sampler.util.NodeUtils;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ChoiceBox;
|
||||
@ -53,7 +52,19 @@ public class ThemePage extends AbstractPage {
|
||||
private ContrastCheckerDialog contrastCheckerDialog;
|
||||
|
||||
@Override
|
||||
public String getName() { return NAME; }
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDisplaySourceCode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canChangeThemeSettings() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public ThemePage() {
|
||||
super();
|
||||
@ -81,7 +92,7 @@ public class ThemePage extends AbstractPage {
|
||||
var noteText = new TextFlow(
|
||||
new Text("AtlantaFX follows "),
|
||||
hyperlink("Github Primer interface guidelines",
|
||||
URI.create("https://primer.style/design/foundations/color")
|
||||
URI.create("https://primer.style/design/foundations/color")
|
||||
),
|
||||
new Text(" and color system.")
|
||||
);
|
||||
@ -94,11 +105,6 @@ public class ThemePage extends AbstractPage {
|
||||
);
|
||||
|
||||
selectCurrentTheme();
|
||||
|
||||
// if you want to enable quick menu don't forget that
|
||||
// theme selection choice box have to be updated accordingly
|
||||
NodeUtils.toggleVisibility(quickConfigBtn, false);
|
||||
NodeUtils.toggleVisibility(sourceCodeToggleBtn, false);
|
||||
}
|
||||
|
||||
private GridPane optionsGrid() {
|
||||
|
@ -38,7 +38,19 @@ public class TypographyPage extends AbstractPage {
|
||||
public static final String NAME = "Typography";
|
||||
|
||||
@Override
|
||||
public String getName() { return NAME; }
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDisplaySourceCode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canChangeThemeSettings() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private Pane fontSizeSampleContent;
|
||||
|
||||
@ -74,10 +86,6 @@ public class TypographyPage extends AbstractPage {
|
||||
textColorSample().getRoot(),
|
||||
textFlowSample().getRoot()
|
||||
);
|
||||
// if you want to enable quick menu don't forget that
|
||||
// font size spinner value have to be updated accordingly
|
||||
NodeUtils.toggleVisibility(quickConfigBtn, false);
|
||||
NodeUtils.toggleVisibility(sourceCodeToggleBtn, false);
|
||||
}
|
||||
|
||||
private ComboBox<String> fontFamilyChooser() {
|
||||
|
@ -29,6 +29,11 @@ public abstract class ShowcasePage extends AbstractPage {
|
||||
createShowcaseLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDisplaySourceCode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void createShowcaseLayout() {
|
||||
var expandBtn = new Button("Expand");
|
||||
expandBtn.setGraphic(new FontIcon(Feather.MAXIMIZE_2));
|
||||
@ -59,9 +64,6 @@ public abstract class ShowcasePage extends AbstractPage {
|
||||
collapseBox.setVisible(false);
|
||||
collapseBox.setManaged(false);
|
||||
|
||||
sourceCodeToggleBtn.setVisible(false);
|
||||
sourceCodeToggleBtn.setManaged(false);
|
||||
|
||||
userContent.getChildren().setAll(showcase, expandBox, collapseBox);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
app.name=AtlantaFX Sampler
|
||||
app.description=${project.description}
|
||||
app.homepage=${project.url}
|
||||
app.homepage=${project.parent.url}
|
||||
app.version=${project.version}
|
||||
|
@ -22,11 +22,8 @@
|
||||
}
|
||||
|
||||
.page {
|
||||
>.header {
|
||||
@include hide();
|
||||
}
|
||||
|
||||
>.stack>.scroll-pane>.viewport>*>.wrapper>.user-content {
|
||||
>.scroll-pane>.viewport>*>.wrapper>.user-content {
|
||||
-fx-max-width: 4096px;
|
||||
-fx-padding: 0;
|
||||
-fx-spacing: 0;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@use "sidebar";
|
||||
@use "main";
|
||||
@use "overlay";
|
||||
@use "page";
|
||||
@use "components";
|
||||
@use "overlay";
|
||||
@use "components";
|
125
sampler/src/main/resources/assets/styles/scss/layout/_main.scss
Normal file
125
sampler/src/main/resources/assets/styles/scss/layout/_main.scss
Normal file
@ -0,0 +1,125 @@
|
||||
@mixin link-button() {
|
||||
-fx-icon-color: -color-fg-emphasis;
|
||||
-fx-fill: -color-fg-emphasis;
|
||||
-fx-icon-size: 20px;
|
||||
|
||||
&:hover {
|
||||
-fx-opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
#main {
|
||||
.source-code {
|
||||
-fx-background-color: -color-bg-default;
|
||||
-fx-alignment: TOP_CENTER;
|
||||
|
||||
>.code-viewer {
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding: 0 0 0 20px;
|
||||
-fx-max-width: 1000px;
|
||||
|
||||
.web-view {
|
||||
-fx-padding: 0;
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#header-bar {
|
||||
-fx-background-color: -color-accent-emphasis;
|
||||
-fx-padding: 0px 30px 0px 0;
|
||||
-fx-spacing: 30px;
|
||||
|
||||
>.logo {
|
||||
-fx-padding: 0px 0 0px 20px;
|
||||
|
||||
>.label {
|
||||
-fx-text-fill: -color-fg-emphasis;
|
||||
}
|
||||
|
||||
>.version {
|
||||
-fx-padding: -1em 0 0 0;
|
||||
}
|
||||
|
||||
>.ikonli-font-icon {
|
||||
@include link-button();
|
||||
}
|
||||
}
|
||||
|
||||
>.text-field {
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 0;
|
||||
-fx-opacity: 0.85;
|
||||
|
||||
-color-input-bg: -color-accent-emphasis;
|
||||
-color-input-bg-focused: -color-accent-emphasis;
|
||||
-color-input-fg: -color-fg-emphasis;
|
||||
-color-input-border: -color-fg-emphasis;
|
||||
-color-input-border-focused: -color-fg-emphasis;
|
||||
-fx-prompt-text-fill: -color-fg-emphasis;
|
||||
|
||||
.ikonli-font-icon {
|
||||
-fx-icon-color: -color-fg-emphasis;
|
||||
-fx-fill: -color-fg-emphasis;
|
||||
}
|
||||
}
|
||||
|
||||
>.page-title {
|
||||
-fx-text-fill: -color-fg-emphasis;
|
||||
-fx-padding: 0 0 0 30px;
|
||||
}
|
||||
|
||||
>.ikonli-font-icon {
|
||||
@include link-button();
|
||||
|
||||
&:disabled {
|
||||
-fx-opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
>.dev-mode-indicator {
|
||||
-fx-background-color: -color-warning-emphasis;
|
||||
-fx-text-fill: -color-fg-emphasis;
|
||||
-fx-background-radius: 10px;
|
||||
-fx-padding: 5px 10px 5px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
-fx-border-color: -color-border-default;
|
||||
-fx-border-width: 0 1px 0 0;
|
||||
|
||||
>.scroll-pane {
|
||||
-fx-background-color: -color-bg-inset;
|
||||
-fx-padding: 0 16px 10px 16px;
|
||||
}
|
||||
|
||||
&:filtered {
|
||||
>.scroll-pane {
|
||||
-fx-padding: 10px 16px 10px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
>.caption {
|
||||
-fx-padding: 18px 0 10px 0;
|
||||
-fx-font-weight: bold;
|
||||
-fx-text-fill: -color-fg-muted;
|
||||
}
|
||||
|
||||
>.nav-link {
|
||||
-fx-padding: 6px 8px 6px 8px;
|
||||
|
||||
&:hover {
|
||||
-fx-background-color: -color-accent-muted;
|
||||
-fx-background-radius: 6px;
|
||||
}
|
||||
|
||||
&:selected {
|
||||
-fx-text-fill: -color-accent-fg;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +1,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
.page {
|
||||
>.header {
|
||||
-fx-padding: 10px 20px 14px 20px;
|
||||
-fx-spacing: 10px;
|
||||
|
||||
>.scroll-pane {
|
||||
-fx-background-color: -color-bg-default;
|
||||
|
||||
>.dev-mode-indicator {
|
||||
-fx-background-color: -color-warning-subtle;
|
||||
-fx-background-radius: 10px;
|
||||
-fx-text-fill: -color-warning-fg;
|
||||
-fx-padding: 5px;
|
||||
-fx-border-color: -color-warning-muted;
|
||||
-fx-border-width: 1px;
|
||||
-fx-border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
>.stack {
|
||||
>.scroll-pane {
|
||||
-fx-background-color: -color-bg-default;
|
||||
|
||||
/* wrapper is used to center the content and also guarantees some minimum paddings via min-width */
|
||||
>.viewport>*>.wrapper {
|
||||
-fx-min-width: 880px;
|
||||
-fx-alignment: TOP_CENTER;
|
||||
|
||||
>.user-content {
|
||||
-fx-padding: 40px 0 40px 0;
|
||||
-fx-spacing: 40px;
|
||||
-fx-max-width: 800px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>.wrapper {
|
||||
-fx-background-color: -color-bg-default;
|
||||
>.viewport>*>.wrapper {
|
||||
-fx-min-width: 880px;
|
||||
-fx-alignment: TOP_CENTER;
|
||||
|
||||
>.code-viewer {
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding: 0px 0px 20px 20px;
|
||||
-fx-max-width: 1000px;
|
||||
|
||||
>.web-view {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
>.user-content {
|
||||
-fx-padding: 40px 0 40px 0;
|
||||
-fx-spacing: 40px;
|
||||
-fx-max-width: 800px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#sidebar {
|
||||
-fx-padding: 0 0 12px 0;
|
||||
-fx-background-color: -color-bg-inset;
|
||||
-fx-border-color: -color-border-default;
|
||||
-fx-border-width: 0 1px 0 0;
|
||||
|
||||
#search-form {
|
||||
-fx-padding: 12px;
|
||||
-fx-border-color: -color-border-muted;
|
||||
-fx-border-width: 0 0 1px 0;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
-fx-padding: 0 6px 0 6px;
|
||||
|
||||
>.caption {
|
||||
-fx-padding: 10px 0 10px 6px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-text-fill: -color-fg-muted;
|
||||
}
|
||||
|
||||
>.nav-link {
|
||||
-fx-padding: 6px 12px 6px 12px;
|
||||
|
||||
>.tag {
|
||||
-fx-background-color: -color-accent-emphasis;
|
||||
-fx-text-fill: -color-fg-emphasis;
|
||||
-fx-padding: 2px;
|
||||
-fx-background-radius: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
-fx-background-color: -color-accent-muted;
|
||||
-fx-background-radius: 6px;
|
||||
}
|
||||
|
||||
&:selected {
|
||||
-fx-text-fill: -color-accent-fg;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user