Rewrite sidebar to TreeView

This commit is contained in:
mkpaz 2023-05-01 19:16:39 +04:00
parent 188ee75986
commit 6584c53906
11 changed files with 698 additions and 349 deletions

@ -28,7 +28,6 @@ import javafx.scene.SceneAntialiasing;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.stage.Stage; import javafx.stage.Stage;
@ -39,7 +38,7 @@ public class Launcher extends Application {
); );
public static final List<KeyCodeCombination> SUPPORTED_HOTKEYS = List.of( public static final List<KeyCodeCombination> SUPPORTED_HOTKEYS = List.of(
new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN) new KeyCodeCombination(KeyCode.SLASH)
); );
public static void main(String[] args) { public static void main(String[] args) {

@ -8,26 +8,30 @@ import static atlantafx.base.theme.Styles.TITLE_4;
import static atlantafx.sampler.Launcher.IS_DEV_MODE; import static atlantafx.sampler.Launcher.IS_DEV_MODE;
import static atlantafx.sampler.layout.MainLayer.SIDEBAR_WIDTH; import static atlantafx.sampler.layout.MainLayer.SIDEBAR_WIDTH;
import atlantafx.base.controls.CustomTextField;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.sampler.Resources; import atlantafx.sampler.Resources;
import atlantafx.sampler.event.BrowseEvent; import atlantafx.sampler.event.BrowseEvent;
import atlantafx.sampler.event.DefaultEventBus; import atlantafx.sampler.event.DefaultEventBus;
import atlantafx.sampler.event.HotkeyEvent; import atlantafx.sampler.event.HotkeyEvent;
import java.net.URI; import java.net.URI;
import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.geometry.HPos;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCombination.ModifierValue;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import org.kordamp.ikonli.Ikon; import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
@ -42,10 +46,14 @@ class HeaderBar extends HBox {
private final MainModel model; private final MainModel model;
private Consumer<Node> quickConfigActionHandler; private Consumer<Node> quickConfigActionHandler;
private Overlay overlay;
private SearchDialog searchDialog;
public HeaderBar(MainModel model) { public HeaderBar(MainModel model) {
super(); super();
this.model = model; this.model = model;
createView(); createView();
} }
@ -80,14 +88,24 @@ class HeaderBar extends HBox {
titleLabel.getStyleClass().addAll("page-title", TITLE_4); titleLabel.getStyleClass().addAll("page-title", TITLE_4);
titleLabel.textProperty().bind(model.titleProperty()); titleLabel.textProperty().bind(model.titleProperty());
var searchField = new CustomTextField(); var searchBox = new HBox(10,
searchField.setLeft(new FontIcon(Material2MZ.SEARCH)); new FontIcon(Material2MZ.SEARCH),
searchField.setPromptText("Search"); new Text("Search"),
model.searchTextProperty().bind(searchField.textProperty()); new Spacer(5),
new Label("/")
);
searchBox.getStyleClass().add("box");
searchBox.setAlignment(Pos.CENTER_LEFT);
var searchButton = new Button();
searchButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
searchButton.getStyleClass().add("search-button");
searchButton.setGraphic(searchBox);
searchButton.setOnAction(e -> openSearchDialog());
DefaultEventBus.getInstance().subscribe(HotkeyEvent.class, e -> { DefaultEventBus.getInstance().subscribe(HotkeyEvent.class, e -> {
if (e.getKeys().getControl() == ModifierValue.DOWN && e.getKeys().getCode() == KeyCode.F) { if (e.getKeys().getCode() == KeyCode.SLASH) {
searchField.requestFocus(); openSearchDialog();
} }
}); });
@ -139,7 +157,7 @@ class HeaderBar extends HBox {
logoBox, logoBox,
titleLabel, titleLabel,
new Spacer(), new Spacer(),
searchField, searchButton,
popoverAnchor, popoverAnchor,
quickConfigBtn, quickConfigBtn,
sourceCodeBtn, sourceCodeBtn,
@ -156,4 +174,26 @@ class HeaderBar extends HBox {
void setQuickConfigActionHandler(Consumer<Node> handler) { void setQuickConfigActionHandler(Consumer<Node> handler) {
this.quickConfigActionHandler = handler; this.quickConfigActionHandler = handler;
} }
private Overlay lookupOverlay() {
return Objects.requireNonNullElse(overlay,
overlay = getScene() != null && getScene().lookup("." + Overlay.STYLE_CLASS) instanceof Overlay o ? o : null
);
}
private void openSearchDialog() {
if (searchDialog == null) {
searchDialog = new SearchDialog(model);
searchDialog.setOnCloseRequest(() -> {
var overlay = lookupOverlay();
overlay.removeContent();
overlay.toBack();
});
}
var overlay = lookupOverlay();
overlay.setContent(searchDialog, HPos.CENTER);
overlay.toFront();
Platform.runLater(() -> searchDialog.begForFocus());
}
} }

@ -12,7 +12,6 @@ import atlantafx.sampler.layout.MainModel.SubLayer;
import atlantafx.sampler.page.CodeViewer; import atlantafx.sampler.page.CodeViewer;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.QuickConfigMenu; import atlantafx.sampler.page.QuickConfigMenu;
import atlantafx.sampler.page.components.OverviewPage;
import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.theme.ThemeManager;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; import java.util.Objects;
@ -27,7 +26,7 @@ import javafx.util.Duration;
class MainLayer extends BorderPane { class MainLayer extends BorderPane {
static final int SIDEBAR_WIDTH = 220; static final int SIDEBAR_WIDTH = 250;
static final int PAGE_TRANSITION_DURATION = 500; // ms static final int PAGE_TRANSITION_DURATION = 500; // ms
private final MainModel model = new MainModel(); private final MainModel model = new MainModel();
@ -45,13 +44,15 @@ class MainLayer extends BorderPane {
createView(); createView();
initListeners(); initListeners();
model.navigate(OverviewPage.class); model.navigate(MainModel.DEFAULT_PAGE);
// keyboard navigation won't work without focus // keyboard navigation won't work without focus
Platform.runLater(sidebar::begForFocus); Platform.runLater(sidebar::begForFocus);
} }
private void createView() { private void createView() {
sidebar.setMinWidth(SIDEBAR_WIDTH); sidebar.setMinWidth(SIDEBAR_WIDTH);
sidebar.setMaxWidth(SIDEBAR_WIDTH);
codeViewer = new CodeViewer(); codeViewer = new CodeViewer();

@ -6,6 +6,52 @@ import static atlantafx.sampler.layout.MainModel.SubLayer.PAGE;
import static atlantafx.sampler.layout.MainModel.SubLayer.SOURCE_CODE; import static atlantafx.sampler.layout.MainModel.SubLayer.SOURCE_CODE;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.components.AccordionPage;
import atlantafx.sampler.page.components.BreadcrumbsPage;
import atlantafx.sampler.page.components.ButtonPage;
import atlantafx.sampler.page.components.ChartPage;
import atlantafx.sampler.page.components.CheckBoxPage;
import atlantafx.sampler.page.components.ColorPickerPage;
import atlantafx.sampler.page.components.ComboBoxPage;
import atlantafx.sampler.page.components.CustomTextFieldPage;
import atlantafx.sampler.page.components.DatePickerPage;
import atlantafx.sampler.page.components.DialogPage;
import atlantafx.sampler.page.components.HtmlEditorPage;
import atlantafx.sampler.page.components.InputGroupPage;
import atlantafx.sampler.page.components.LabelPage;
import atlantafx.sampler.page.components.ListPage;
import atlantafx.sampler.page.components.MenuButtonPage;
import atlantafx.sampler.page.components.MenuPage;
import atlantafx.sampler.page.components.OverviewPage;
import atlantafx.sampler.page.components.PaginationPage;
import atlantafx.sampler.page.components.PopoverPage;
import atlantafx.sampler.page.components.ProgressPage;
import atlantafx.sampler.page.components.RadioButtonPage;
import atlantafx.sampler.page.components.ScrollPanePage;
import atlantafx.sampler.page.components.SeparatorPage;
import atlantafx.sampler.page.components.SliderPage;
import atlantafx.sampler.page.components.SpinnerPage;
import atlantafx.sampler.page.components.SplitPanePage;
import atlantafx.sampler.page.components.TabPanePage;
import atlantafx.sampler.page.components.TablePage;
import atlantafx.sampler.page.components.TextAreaPage;
import atlantafx.sampler.page.components.TextFieldPage;
import atlantafx.sampler.page.components.TitledPanePage;
import atlantafx.sampler.page.components.ToggleButtonPage;
import atlantafx.sampler.page.components.ToggleSwitchPage;
import atlantafx.sampler.page.components.ToolBarPage;
import atlantafx.sampler.page.components.TooltipPage;
import atlantafx.sampler.page.components.TreePage;
import atlantafx.sampler.page.components.TreeTablePage;
import atlantafx.sampler.page.general.IconsPage;
import atlantafx.sampler.page.general.ThemePage;
import atlantafx.sampler.page.general.TypographyPage;
import atlantafx.sampler.page.showcase.filemanager.FileManagerPage;
import atlantafx.sampler.page.showcase.musicplayer.MusicPlayerPage;
import atlantafx.sampler.page.showcase.widget.WidgetCollectionPage;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyBooleanWrapper;
@ -15,49 +61,221 @@ import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2OutlinedAL;
import org.kordamp.ikonli.material2.Material2OutlinedMZ;
public class MainModel { public class MainModel {
public static Class<? extends Page> DEFAULT_PAGE = OverviewPage.class;
private static final Map<Class<? extends Page>, NavTree.Item> NAV_TREE = createNavItems();
public enum SubLayer { public enum SubLayer {
PAGE, PAGE,
SOURCE_CODE SOURCE_CODE
} }
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(); NavTree.Item getTreeItemForPage(Class<? extends Page> pageClass) {
private final StringProperty searchText = new SimpleStringProperty(); return NAV_TREE.getOrDefault(pageClass, NAV_TREE.get(DEFAULT_PAGE));
private final ReadOnlyObjectWrapper<Class<? extends Page>> selectedPage = new ReadOnlyObjectWrapper<>(); }
private final ReadOnlyBooleanWrapper themeChangeToggle = new ReadOnlyBooleanWrapper();
private final ReadOnlyBooleanWrapper sourceCodeToggle = new ReadOnlyBooleanWrapper(); List<NavTree.Item> findPages(String filter) {
private final ReadOnlyObjectWrapper<SubLayer> currentSubLayer = new ReadOnlyObjectWrapper<>(PAGE); return NAV_TREE.values().stream()
.filter(item -> item.getValue() != null && item.getValue().matches(filter))
.toList();
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Properties // // Properties //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
private final StringProperty searchText = new SimpleStringProperty();
public StringProperty searchTextProperty() { public StringProperty searchTextProperty() {
return searchText; return searchText;
} }
// ~
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper();
public ReadOnlyStringProperty titleProperty() { public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty(); return title.getReadOnlyProperty();
} }
// ~
private final ReadOnlyBooleanWrapper themeChangeToggle = new ReadOnlyBooleanWrapper();
public ReadOnlyBooleanProperty themeChangeToggleProperty() { public ReadOnlyBooleanProperty themeChangeToggleProperty() {
return themeChangeToggle.getReadOnlyProperty(); return themeChangeToggle.getReadOnlyProperty();
} }
// ~
private final ReadOnlyBooleanWrapper sourceCodeToggle = new ReadOnlyBooleanWrapper();
public ReadOnlyBooleanProperty sourceCodeToggleProperty() { public ReadOnlyBooleanProperty sourceCodeToggleProperty() {
return sourceCodeToggle.getReadOnlyProperty(); return sourceCodeToggle.getReadOnlyProperty();
} }
// ~
private final ReadOnlyObjectWrapper<Class<? extends Page>> selectedPage = new ReadOnlyObjectWrapper<>();
public ReadOnlyObjectProperty<Class<? extends Page>> selectedPageProperty() { public ReadOnlyObjectProperty<Class<? extends Page>> selectedPageProperty() {
return selectedPage.getReadOnlyProperty(); return selectedPage.getReadOnlyProperty();
} }
// ~
private final ReadOnlyObjectWrapper<SubLayer> currentSubLayer = new ReadOnlyObjectWrapper<>(PAGE);
public ReadOnlyObjectProperty<SubLayer> currentSubLayerProperty() { public ReadOnlyObjectProperty<SubLayer> currentSubLayerProperty() {
return currentSubLayer.getReadOnlyProperty(); return currentSubLayer.getReadOnlyProperty();
} }
// ~
private final ReadOnlyObjectWrapper<NavTree.Item> navTree = new ReadOnlyObjectWrapper<>(createTree());
public ReadOnlyObjectProperty<NavTree.Item> navTreeProperty() {
return navTree.getReadOnlyProperty();
}
private NavTree.Item createTree() {
var general = NavTree.Item.group("General", new FontIcon(Material2OutlinedAL.ARTICLE));
general.getChildren().setAll(
NAV_TREE.get(OverviewPage.class),
NAV_TREE.get(ThemePage.class),
NAV_TREE.get(TypographyPage.class),
NAV_TREE.get(IconsPage.class)
);
general.setExpanded(true);
var components = NavTree.Item.group("Standard Controls", new FontIcon(Material2OutlinedAL.DASHBOARD));
components.getChildren().setAll(
NAV_TREE.get(AccordionPage.class),
NAV_TREE.get(ButtonPage.class),
NAV_TREE.get(ChartPage.class),
NAV_TREE.get(CheckBoxPage.class),
NAV_TREE.get(ColorPickerPage.class),
NAV_TREE.get(ComboBoxPage.class),
NAV_TREE.get(DatePickerPage.class),
NAV_TREE.get(DialogPage.class),
NAV_TREE.get(HtmlEditorPage.class),
NAV_TREE.get(LabelPage.class),
NAV_TREE.get(ListPage.class),
NAV_TREE.get(MenuPage.class),
NAV_TREE.get(MenuButtonPage.class),
NAV_TREE.get(PaginationPage.class),
NAV_TREE.get(ProgressPage.class),
NAV_TREE.get(RadioButtonPage.class),
NAV_TREE.get(ScrollPanePage.class),
NAV_TREE.get(SeparatorPage.class),
NAV_TREE.get(SliderPage.class),
NAV_TREE.get(SpinnerPage.class),
NAV_TREE.get(SplitPanePage.class),
NAV_TREE.get(TablePage.class),
NAV_TREE.get(TabPanePage.class),
NAV_TREE.get(TextAreaPage.class),
NAV_TREE.get(TextFieldPage.class),
NAV_TREE.get(TitledPanePage.class),
NAV_TREE.get(ToggleButtonPage.class),
NAV_TREE.get(ToolBarPage.class),
NAV_TREE.get(TooltipPage.class),
NAV_TREE.get(TreePage.class),
NAV_TREE.get(TreeTablePage.class)
);
var extras = NavTree.Item.group("Extras", new FontIcon(Material2OutlinedMZ.TOGGLE_ON));
extras.getChildren().setAll(
NAV_TREE.get(InputGroupPage.class),
NAV_TREE.get(BreadcrumbsPage.class),
NAV_TREE.get(CustomTextFieldPage.class),
NAV_TREE.get(PopoverPage.class),
NAV_TREE.get(ToggleSwitchPage.class)
);
var showcases = NavTree.Item.group("Showcase", new FontIcon(Material2OutlinedMZ.VISIBILITY));
showcases.getChildren().setAll(
NAV_TREE.get(FileManagerPage.class),
NAV_TREE.get(MusicPlayerPage.class),
NAV_TREE.get(WidgetCollectionPage.class)
);
var root = NavTree.Item.root();
root.getChildren().setAll(general, components, extras, showcases);
return root;
}
///////////////////////////////////////////////////////////////////////////
// Nav Tree //
///////////////////////////////////////////////////////////////////////////
public static Map<Class<? extends Page>, NavTree.Item> createNavItems() {
var map = new HashMap<Class<? extends Page>, NavTree.Item>();
map.put(OverviewPage.class, NavTree.Item.page(OverviewPage.NAME, OverviewPage.class));
map.put(ThemePage.class, NavTree.Item.page(ThemePage.NAME, ThemePage.class));
map.put(TypographyPage.class, NavTree.Item.page(TypographyPage.NAME, TypographyPage.class));
map.put(IconsPage.class, NavTree.Item.page(IconsPage.NAME, IconsPage.class));
map.put(InputGroupPage.class, NavTree.Item.page(InputGroupPage.NAME, InputGroupPage.class));
map.put(AccordionPage.class, NavTree.Item.page(AccordionPage.NAME, AccordionPage.class));
map.put(BreadcrumbsPage.class, NavTree.Item.page(BreadcrumbsPage.NAME, BreadcrumbsPage.class));
map.put(ButtonPage.class, NavTree.Item.page(ButtonPage.NAME, ButtonPage.class));
map.put(ChartPage.class, NavTree.Item.page(ChartPage.NAME, ChartPage.class));
map.put(CheckBoxPage.class, NavTree.Item.page(CheckBoxPage.NAME, CheckBoxPage.class));
map.put(ColorPickerPage.class, NavTree.Item.page(ColorPickerPage.NAME, ColorPickerPage.class));
map.put(
ComboBoxPage.class,
NavTree.Item.page(ComboBoxPage.NAME, ComboBoxPage.class, "ChoiceBox")
);
map.put(
CustomTextFieldPage.class,
NavTree.Item.page(CustomTextFieldPage.NAME, CustomTextFieldPage.class, "MaskTextField", "PasswordTextField")
);
map.put(DatePickerPage.class, NavTree.Item.page(DatePickerPage.NAME, DatePickerPage.class));
map.put(DialogPage.class, NavTree.Item.page(DialogPage.NAME, DialogPage.class));
map.put(HtmlEditorPage.class, NavTree.Item.page(HtmlEditorPage.NAME, HtmlEditorPage.class));
map.put(LabelPage.class, NavTree.Item.page(LabelPage.NAME, LabelPage.class));
map.put(ListPage.class, NavTree.Item.page(ListPage.NAME, ListPage.class));
map.put(MenuPage.class, NavTree.Item.page(MenuPage.NAME, MenuPage.class));
map.put(MenuButtonPage.class, NavTree.Item.page(
MenuButtonPage.NAME,
MenuButtonPage.class, "SplitMenuButton")
);
map.put(PaginationPage.class, NavTree.Item.page(PaginationPage.NAME, PaginationPage.class));
map.put(PopoverPage.class, NavTree.Item.page(PopoverPage.NAME, PopoverPage.class));
map.put(ProgressPage.class, NavTree.Item.page(ProgressPage.NAME, ProgressPage.class));
map.put(RadioButtonPage.class, NavTree.Item.page(RadioButtonPage.NAME, RadioButtonPage.class));
map.put(ScrollPanePage.class, NavTree.Item.page(ScrollPanePage.NAME, ScrollPanePage.class));
map.put(SeparatorPage.class, NavTree.Item.page(SeparatorPage.NAME, SeparatorPage.class));
map.put(SliderPage.class, NavTree.Item.page(SliderPage.NAME, SliderPage.class));
map.put(SpinnerPage.class, NavTree.Item.page(SpinnerPage.NAME, SpinnerPage.class));
map.put(SplitPanePage.class, NavTree.Item.page(SplitPanePage.NAME, SplitPanePage.class));
map.put(TablePage.class, NavTree.Item.page(TablePage.NAME, TablePage.class));
map.put(TabPanePage.class, NavTree.Item.page(TabPanePage.NAME, TabPanePage.class));
map.put(TextAreaPage.class, NavTree.Item.page(TextAreaPage.NAME, TextAreaPage.class));
map.put(TextFieldPage.class, NavTree.Item.page(
TextFieldPage.NAME,
TextFieldPage.class, "PasswordField")
);
map.put(TitledPanePage.class, NavTree.Item.page(TitledPanePage.NAME, TitledPanePage.class));
map.put(ToggleButtonPage.class, NavTree.Item.page(ToggleButtonPage.NAME, ToggleButtonPage.class));
map.put(ToggleSwitchPage.class, NavTree.Item.page(ToggleSwitchPage.NAME, ToggleSwitchPage.class));
map.put(ToolBarPage.class, NavTree.Item.page(ToolBarPage.NAME, ToolBarPage.class));
map.put(TooltipPage.class, NavTree.Item.page(TooltipPage.NAME, TooltipPage.class));
map.put(TreePage.class, NavTree.Item.page(TreePage.NAME, TreePage.class));
map.put(TreeTablePage.class, NavTree.Item.page(TreeTablePage.NAME, TreeTablePage.class));
map.put(FileManagerPage.class, NavTree.Item.page(FileManagerPage.NAME, FileManagerPage.class));
map.put(MusicPlayerPage.class, NavTree.Item.page(FileManagerPage.NAME, MusicPlayerPage.class));
map.put(WidgetCollectionPage.class, NavTree.Item.page(
FileManagerPage.NAME,
WidgetCollectionPage.class, "Card", "Message", "Stepper", "Tag")
);
return map;
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Commands // // Commands //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////

@ -0,0 +1,37 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.layout;
import atlantafx.sampler.page.Page;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javafx.scene.Node;
import org.jetbrains.annotations.Nullable;
public record Nav(String title,
@Nullable Node graphic,
@Nullable Class<? extends Page> pageClass,
@Nullable List<String> searchKeywords) {
public static final Nav ROOT = new Nav("ROOT", null, null, null);
public Nav {
Objects.requireNonNull(title, "title");
searchKeywords = Objects.requireNonNullElse(searchKeywords, Collections.emptyList());
}
public boolean isGroup() {
return pageClass == null;
}
public boolean matches(String filter) {
Objects.requireNonNull(filter);
return contains(title, filter)
|| (searchKeywords != null && searchKeywords.stream().anyMatch(keyword -> contains(keyword, filter)));
}
private boolean contains(String text, String filter) {
return text.toLowerCase().contains(filter.toLowerCase());
}
}

@ -0,0 +1,143 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.layout;
import atlantafx.base.controls.Spacer;
import atlantafx.sampler.page.Page;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.HBox;
import org.jetbrains.annotations.Nullable;
import org.kordamp.ikonli.javafx.FontIcon;
public class NavTree extends TreeView<Nav> {
public NavTree(MainModel model) {
super();
getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (!(val instanceof Item item)) {
return;
}
if (!item.isGroup()) {
model.navigate(item.pageClass());
}
});
setShowRoot(false);
rootProperty().bind(model.navTreeProperty());
setCellFactory(p -> new NavTreeCell());
}
///////////////////////////////////////////////////////////////////////////
public static final class NavTreeCell extends TreeCell<Nav> {
private static final PseudoClass GROUP = PseudoClass.getPseudoClass("group");
private final HBox root;
private final Label titleLabel;
private final Node arrowIcon;
public NavTreeCell() {
super();
titleLabel = new Label();
titleLabel.setGraphicTextGap(10);
titleLabel.getStyleClass().add("title");
arrowIcon = new FontIcon();
arrowIcon.getStyleClass().add("arrow");
root = new HBox();
root.setAlignment(Pos.CENTER_LEFT);
root.getChildren().setAll(titleLabel, new Spacer(), arrowIcon);
root.setCursor(Cursor.HAND);
root.getStyleClass().add("container");
root.setMaxWidth(MainLayer.SIDEBAR_WIDTH - 10);
root.setOnMouseClicked(e -> {
if (!(getTreeItem() instanceof Item item)) {
return;
}
if (item.isGroup() && e.getButton() == MouseButton.PRIMARY) {
item.setExpanded(!item.isExpanded());
// scroll slightly above the target
getTreeView().scrollTo(getTreeView().getRow(item) - 10);
}
});
getStyleClass().add("nav-tree-cell");
}
@Override
protected void updateItem(Nav nav, boolean empty) {
super.updateItem(nav, empty);
if (nav == null || empty) {
setGraphic(null);
titleLabel.setText(null);
titleLabel.setGraphic(null);
} else {
setGraphic(root);
titleLabel.setText(nav.title());
titleLabel.setGraphic(nav.graphic());
pseudoClassStateChanged(GROUP, nav.isGroup());
arrowIcon.setVisible(nav.isGroup());
}
}
}
public static final class Item extends TreeItem<Nav> {
private final Nav nav;
private Item(Nav nav) {
this.nav = Objects.requireNonNull(nav, "nav");
setValue(nav);
}
public boolean isGroup() {
return nav.isGroup();
}
public @Nullable Class<? extends Page> pageClass() {
return nav.pageClass();
}
public static Item root() {
return new Item(Nav.ROOT);
}
public static Item group(String title, Node graphic) {
return new Item(new Nav(title, graphic, null, null));
}
public static Item page(String title,
@Nullable Class<? extends Page> pageClass) {
Objects.requireNonNull(pageClass, "pageClass");
return new Item(new Nav(title, null, pageClass, Collections.emptyList()));
}
public static Item page(String title,
@Nullable Class<? extends Page> pageClass,
String... searchKeywords) {
Objects.requireNonNull(pageClass, "pageClass");
return new Item(new Nav(title, null, pageClass, List.of(searchKeywords)));
}
}
}

@ -0,0 +1,149 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.layout;
import atlantafx.base.controls.CustomTextField;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.page.OverlayDialog;
import java.util.function.Consumer;
import javafx.event.EventType;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
import org.kordamp.ikonli.material2.Material2MZ;
final class SearchDialog extends OverlayDialog<VBox> {
private final MainModel model;
private CustomTextField searchField;
private ListView<NavTree.Item> resultList;
public SearchDialog(MainModel model) {
super();
this.model = model;
setId("search-dialog");
setTitle("Search");
setContent(createContent());
init();
}
private VBox createContent() {
var placeholder = new Label("Your search results will appear here");
placeholder.getStyleClass().add(Styles.TITLE_4);
searchField = new CustomTextField();
searchField.setLeft(new FontIcon(Material2MZ.SEARCH));
VBox.setVgrow(searchField, Priority.NEVER);
Consumer<NavTree.Item> clickHandler = item -> {
if (item.pageClass() != null) {
close();
model.navigate(item.pageClass());
}
};
resultList = new ListView<>();
resultList.setPlaceholder(placeholder);
resultList.getStyleClass().add(Tweaks.EDGE_TO_EDGE);
resultList.setCellFactory(c -> new ResultListCell(clickHandler));
VBox.setVgrow(resultList, Priority.ALWAYS);
var content = new VBox(10, searchField, resultList);
content.setPadding(new Insets(10, 20, 10, 20));
content.setPrefSize(600, 440);
return content;
}
private void init() {
searchField.textProperty().addListener((obs, old, val) -> {
if (val == null || val.length() <= 2) {
resultList.getItems().clear();
return;
}
resultList.getItems().setAll(model.findPages(val));
});
searchField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.DOWN && !resultList.getItems().isEmpty()) {
resultList.getSelectionModel().selectFirst();
resultList.requestFocus();
}
});
resultList.setOnKeyPressed(e -> {
var selectionModel = resultList.getSelectionModel();
if (e.getCode() == KeyCode.ENTER && !selectionModel.isEmpty()) {
close();
model.navigate(selectionModel.getSelectedItem().pageClass());
}
});
}
void begForFocus() {
searchField.requestFocus();
}
///////////////////////////////////////////////////////////////////////////
public static final class ResultListCell extends ListCell<NavTree.Item> {
private final HBox root;
private final Label parentLabel;
private final Label targetLabel;
public ResultListCell(Consumer<NavTree.Item> clickHandler) {
super();
parentLabel = new Label();
parentLabel.getStyleClass().add(Styles.TEXT_MUTED);
var separatorIcon = new FontIcon(Material2AL.CHEVRON_RIGHT);
separatorIcon.getStyleClass().add("icon-subtle");
var returnIcon = new FontIcon(Material2AL.KEYBOARD_RETURN);
returnIcon.getStyleClass().add("icon-subtle");
targetLabel = new Label();
targetLabel.getStyleClass().add(Styles.TEXT_BOLD);
root = new HBox(parentLabel, separatorIcon, targetLabel, new Spacer(), returnIcon);
root.setAlignment(Pos.CENTER_LEFT);
setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
clickHandler.accept(getItem());
}
});
}
@Override
protected void updateItem(NavTree.Item item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
parentLabel.setText(item.getParent().getValue().title());
targetLabel.setText(item.getValue().title());
setGraphic(root);
}
}
}
}

@ -2,331 +2,34 @@
package atlantafx.sampler.layout; package atlantafx.sampler.layout;
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED;
import static javafx.scene.layout.Priority.ALWAYS;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.components.AccordionPage;
import atlantafx.sampler.page.components.BreadcrumbsPage;
import atlantafx.sampler.page.components.ButtonPage;
import atlantafx.sampler.page.components.ChartPage;
import atlantafx.sampler.page.components.CheckBoxPage;
import atlantafx.sampler.page.components.ColorPickerPage;
import atlantafx.sampler.page.components.ComboBoxPage;
import atlantafx.sampler.page.components.CustomTextFieldPage;
import atlantafx.sampler.page.components.DatePickerPage;
import atlantafx.sampler.page.components.DialogPage;
import atlantafx.sampler.page.components.HtmlEditorPage;
import atlantafx.sampler.page.components.InputGroupPage;
import atlantafx.sampler.page.components.LabelPage;
import atlantafx.sampler.page.components.ListPage;
import atlantafx.sampler.page.components.MenuButtonPage;
import atlantafx.sampler.page.components.MenuPage;
import atlantafx.sampler.page.components.OverviewPage;
import atlantafx.sampler.page.components.PaginationPage;
import atlantafx.sampler.page.components.PopoverPage;
import atlantafx.sampler.page.components.ProgressPage;
import atlantafx.sampler.page.components.RadioButtonPage;
import atlantafx.sampler.page.components.ScrollPanePage;
import atlantafx.sampler.page.components.SeparatorPage;
import atlantafx.sampler.page.components.SliderPage;
import atlantafx.sampler.page.components.SpinnerPage;
import atlantafx.sampler.page.components.SplitPanePage;
import atlantafx.sampler.page.components.TabPanePage;
import atlantafx.sampler.page.components.TablePage;
import atlantafx.sampler.page.components.TextAreaPage;
import atlantafx.sampler.page.components.TextFieldPage;
import atlantafx.sampler.page.components.TitledPanePage;
import atlantafx.sampler.page.components.ToggleButtonPage;
import atlantafx.sampler.page.components.ToggleSwitchPage;
import atlantafx.sampler.page.components.ToolBarPage;
import atlantafx.sampler.page.components.TooltipPage;
import atlantafx.sampler.page.components.TreePage;
import atlantafx.sampler.page.components.TreeTablePage;
import atlantafx.sampler.page.general.IconsPage;
import atlantafx.sampler.page.general.ThemePage;
import atlantafx.sampler.page.general.TypographyPage;
import atlantafx.sampler.page.showcase.filemanager.FileManagerPage;
import atlantafx.sampler.page.showcase.musicplayer.MusicPlayerPage;
import atlantafx.sampler.page.showcase.widget.WidgetCollectionPage;
import atlantafx.sampler.util.Containers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import javafx.beans.binding.Bindings;
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.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.StackPane;
import javafx.scene.layout.VBox;
@SuppressWarnings("UnnecessaryLambda")
class Sidebar extends StackPane { 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 MainModel model; private final MainModel model;
private final NavMenu navMenu; private final NavTree navTree;
private ScrollPane navScroll;
public Sidebar(MainModel model) { public Sidebar(MainModel model) {
super(); super();
this.model = model; this.model = model;
this.navMenu = new NavMenu(model); this.navTree = new NavTree(model);
createView(); createView();
} }
private void createView() { 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(), navMenu.getContent());
navScroll = new ScrollPane(navContainer);
Containers.setScrollConstraints(navScroll, AS_NEEDED, true, AS_NEEDED, true);
VBox.setVgrow(navScroll, ALWAYS);
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));
});
model.selectedPageProperty().addListener((obs, old, val) -> { model.selectedPageProperty().addListener((obs, old, val) -> {
navMenu.findLink(old).ifPresent(link -> link.pseudoClassStateChanged(SELECTED, false)); if (val != null) {
navMenu.findLink(val).ifPresent(link -> link.pseudoClassStateChanged(SELECTED, true)); navTree.getSelectionModel().select(model.getTreeItemForPage(val));
});
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();
}
});
navMenu.getContent().addListener((ListChangeListener<Region>) c -> {
if (navMenu.getContent().isEmpty()) {
placeholder.toFront();
} else {
placeholder.toBack();
} }
}); });
setId("sidebar"); setId("sidebar");
getChildren().addAll(placeholder, navScroll); getChildren().addAll(navTree);
} }
void begForFocus() { void begForFocus() {
navScroll.requestFocus(); navTree.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),
navLink(IconsPage.NAME, IconsPage.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, "MaskTextField", "PasswordTextField"),
navLink(DatePickerPage.NAME, DatePickerPage.class),
navLink(DialogPage.NAME, DialogPage.class),
navLink(HtmlEditorPage.NAME, HtmlEditorPage.class),
navLink(LabelPage.NAME, LabelPage.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),
navLink(WidgetCollectionPage.NAME,
WidgetCollectionPage.class,
"Card", "Message", "Stepper", "Tag"
)
);
}
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) {
super(Objects.requireNonNull(text));
this.pageClass = Objects.requireNonNull(pageClass);
getStyleClass().add("nav-link");
setMaxWidth(Double.MAX_VALUE);
}
public Class<? extends Page> getPageClass() {
return pageClass;
}
public List<String> getSearchKeywords() {
return searchKeywords;
}
public boolean matches(String filter) {
Objects.requireNonNull(filter);
return contains(getText(), filter)
|| searchKeywords.stream().anyMatch(keyword -> contains(keyword, filter));
}
private boolean contains(String text, String filter) {
return text.toLowerCase().contains(filter.toLowerCase());
}
} }
} }

@ -12,6 +12,7 @@ import atlantafx.base.controls.PasswordTextField;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.SampleBlock;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Cursor; import javafx.scene.Cursor;
@ -120,7 +121,7 @@ public class CustomTextFieldPage extends AbstractPage {
var timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); var timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
var timeField = new MaskTextField("29:59"); var timeField = new MaskTextField("29:59");
timeField.setText(LocalTime.now().format(timeFormatter)); timeField.setText(LocalTime.now(ZoneId.systemDefault()).format(timeFormatter));
timeField.setLeft(new FontIcon(Material2OutlinedMZ.TIMER)); timeField.setLeft(new FontIcon(Material2OutlinedMZ.TIMER));
timeField.setPrefWidth(120); timeField.setPrefWidth(120);
timeField.textProperty().addListener((obs, old, val) -> { timeField.textProperty().addListener((obs, old, val) -> {

@ -3,4 +3,9 @@
.bordered { .bordered {
-fx-border-width: 1px; -fx-border-width: 1px;
-fx-border-color: -color-border-muted; -fx-border-color: -color-border-muted;
} }
.icon-subtle {
-fx-fill: -color-fg-subtle;
-fx-icon-color: -color-fg-subtle;
}

@ -66,6 +66,38 @@
} }
} }
>.search-button {
-color-button-bg: -color-accent-emphasis;
-color-button-fg: -color-fg-emphasis;
-color-button-border: transparent;
-color-button-bg-hover: -color-accent-6;
-color-button-fg-hover: -color-fg-emphasis;
-color-button-border-hover: transparent;
-color-button-bg-focused: -color-accent-emphasis;
-color-button-fg-focused: -color-fg-emphasis;
-color-button-border-focused: transparent;
-color-button-bg-pressed: -color-accent-emphasis;
-color-button-fg-pressed: -color-fg-emphasis;
-color-button-border-pressed: transparent;
-color-button-shadow: transparent;
-fx-padding: 6px 16px 6px 12px;
>.box {
>* {
-fx-fill: -color-fg-emphasis;
}
>.label {
-fx-text-fill: -color-fg-emphasis;
-fx-border-color: -color-fg-emphasis;
-fx-border-width: 1;
-fx-padding: 2px 6px 2px 6px;
-fx-opacity: 0.5;
}
}
}
>.page-title { >.page-title {
-fx-text-fill: -color-fg-emphasis; -fx-text-fill: -color-fg-emphasis;
-fx-padding: 0 0 0 30px; -fx-padding: 0 0 0 30px;
@ -84,40 +116,61 @@
} }
#sidebar { #sidebar {
// border is necessary when scrollbar is hidden
// e.g. when displaying search result
-fx-border-color: -color-border-subtle;
-fx-border-width: 0 1px 0 0;
>.scroll-pane { .nav-tree-cell {
-fx-background-color: -color-bg-inset; -fx-padding: 0;
-fx-padding: 0 16px 10px 16px; -fx-indent: 8px;
} -color-cell-bg: -color-bg-inset;
-color-cell-bg-selected: -color-bg-inset;
-color-cell-bg-selected-focused: -color-bg-inset;
-fx-background-radius: 5px;
&:filtered { >.tree-disclosure-node,
>.scroll-pane { >.tree-disclosure-node>.arrow {
-fx-padding: 10px 16px 10px 16px; -fx-min-width: 0;
-fx-pref-width: 0;
-fx-max-width: 0;
-fx-min-height: 0;
-fx-pref-height: 0;
-fx-max-height: 0;
visibility: hidden;
} }
}
.nav-menu { >.container {
>.caption { -fx-min-height: 2.1em;
-fx-padding: 18px 0 10px 0; -fx-pref-height: 2.1em;
-fx-max-height: 2.1em;
-fx-padding: 0 0 0 8px;
}
&:selected >.container >.title {
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-text-fill: -color-fg-muted; -fx-text-fill: -color-accent-fg;
} }
>.nav-link { &:hover:filled {
-fx-padding: 6px 8px 6px 8px; -color-cell-bg: -color-accent-subtle;
}
&:hover { &:group {
-fx-background-color: -color-accent-muted; >.container {
-fx-background-radius: 6px; -fx-min-height: 2.5em;
-fx-pref-height: 2.5em;
-fx-max-height: 2.5em;
-fx-padding: 0 0 0 8px;
.ikonli-font-icon {
-fx-fill: -color-accent-fg;
-fx-icon-color: -color-accent-fg;
}
>.arrow {
-fx-icon-code: mdal-expand_more;
}
} }
&:selected { &:expanded>.container>.arrow {
-fx-text-fill: -color-accent-fg; -fx-icon-code: mdal-chevron_right;
-fx-font-weight: bold;
} }
} }
} }