diff --git a/sampler/src/main/java/atlantafx/sampler/Launcher.java b/sampler/src/main/java/atlantafx/sampler/Launcher.java index df9478e..6c96770 100755 --- a/sampler/src/main/java/atlantafx/sampler/Launcher.java +++ b/sampler/src/main/java/atlantafx/sampler/Launcher.java @@ -28,7 +28,6 @@ import javafx.scene.SceneAntialiasing; 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; @@ -39,7 +38,7 @@ public class Launcher extends Application { ); public static final List SUPPORTED_HOTKEYS = List.of( - new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN) + new KeyCodeCombination(KeyCode.SLASH) ); public static void main(String[] args) { diff --git a/sampler/src/main/java/atlantafx/sampler/layout/HeaderBar.java b/sampler/src/main/java/atlantafx/sampler/layout/HeaderBar.java index 2b03d8a..e2869e7 100644 --- a/sampler/src/main/java/atlantafx/sampler/layout/HeaderBar.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/HeaderBar.java @@ -8,26 +8,30 @@ import static atlantafx.base.theme.Styles.TITLE_4; import static atlantafx.sampler.Launcher.IS_DEV_MODE; import static atlantafx.sampler.layout.MainLayer.SIDEBAR_WIDTH; -import atlantafx.base.controls.CustomTextField; import atlantafx.base.controls.Spacer; import atlantafx.sampler.Resources; import atlantafx.sampler.event.BrowseEvent; import atlantafx.sampler.event.DefaultEventBus; import atlantafx.sampler.event.HotkeyEvent; import java.net.URI; +import java.util.Objects; import java.util.function.Consumer; +import javafx.application.Platform; import javafx.beans.binding.Bindings; +import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyCombination.ModifierValue; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; +import javafx.scene.text.Text; import org.kordamp.ikonli.Ikon; import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.javafx.FontIcon; @@ -42,10 +46,14 @@ class HeaderBar extends HBox { private final MainModel model; private Consumer quickConfigActionHandler; + private Overlay overlay; + private SearchDialog searchDialog; public HeaderBar(MainModel model) { super(); + this.model = model; + createView(); } @@ -80,14 +88,24 @@ class HeaderBar extends HBox { 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()); + var searchBox = new HBox(10, + new FontIcon(Material2MZ.SEARCH), + new Text("Search"), + 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 -> { - if (e.getKeys().getControl() == ModifierValue.DOWN && e.getKeys().getCode() == KeyCode.F) { - searchField.requestFocus(); + if (e.getKeys().getCode() == KeyCode.SLASH) { + openSearchDialog(); } }); @@ -139,7 +157,7 @@ class HeaderBar extends HBox { logoBox, titleLabel, new Spacer(), - searchField, + searchButton, popoverAnchor, quickConfigBtn, sourceCodeBtn, @@ -156,4 +174,26 @@ class HeaderBar extends HBox { void setQuickConfigActionHandler(Consumer 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()); + } } diff --git a/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java b/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java index 2eb066d..34a91b9 100644 --- a/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java @@ -12,7 +12,6 @@ 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 java.io.IOException; import java.util.Objects; @@ -27,7 +26,7 @@ import javafx.util.Duration; class MainLayer extends BorderPane { - static final int SIDEBAR_WIDTH = 220; + static final int SIDEBAR_WIDTH = 250; static final int PAGE_TRANSITION_DURATION = 500; // ms private final MainModel model = new MainModel(); @@ -45,13 +44,15 @@ class MainLayer extends BorderPane { createView(); initListeners(); - model.navigate(OverviewPage.class); + model.navigate(MainModel.DEFAULT_PAGE); + // keyboard navigation won't work without focus Platform.runLater(sidebar::begForFocus); } private void createView() { sidebar.setMinWidth(SIDEBAR_WIDTH); + sidebar.setMaxWidth(SIDEBAR_WIDTH); codeViewer = new CodeViewer(); diff --git a/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java b/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java index db7754e..81da475 100644 --- a/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java @@ -6,6 +6,52 @@ import static atlantafx.sampler.layout.MainModel.SubLayer.PAGE; import static atlantafx.sampler.layout.MainModel.SubLayer.SOURCE_CODE; 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 javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; @@ -15,49 +61,221 @@ import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.property.SimpleStringProperty; 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 static Class DEFAULT_PAGE = OverviewPage.class; + + private static final Map, NavTree.Item> NAV_TREE = createNavItems(); + public enum SubLayer { PAGE, SOURCE_CODE } - private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(); - private final StringProperty searchText = new SimpleStringProperty(); - private final ReadOnlyObjectWrapper> selectedPage = new ReadOnlyObjectWrapper<>(); - private final ReadOnlyBooleanWrapper themeChangeToggle = new ReadOnlyBooleanWrapper(); - private final ReadOnlyBooleanWrapper sourceCodeToggle = new ReadOnlyBooleanWrapper(); - private final ReadOnlyObjectWrapper currentSubLayer = new ReadOnlyObjectWrapper<>(PAGE); + NavTree.Item getTreeItemForPage(Class pageClass) { + return NAV_TREE.getOrDefault(pageClass, NAV_TREE.get(DEFAULT_PAGE)); + } + + List findPages(String filter) { + return NAV_TREE.values().stream() + .filter(item -> item.getValue() != null && item.getValue().matches(filter)) + .toList(); + } /////////////////////////////////////////////////////////////////////////// // Properties // /////////////////////////////////////////////////////////////////////////// + private final StringProperty searchText = new SimpleStringProperty(); + public StringProperty searchTextProperty() { return searchText; } + // ~ + private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(); + public ReadOnlyStringProperty titleProperty() { return title.getReadOnlyProperty(); } + // ~ + private final ReadOnlyBooleanWrapper themeChangeToggle = new ReadOnlyBooleanWrapper(); + public ReadOnlyBooleanProperty themeChangeToggleProperty() { return themeChangeToggle.getReadOnlyProperty(); } + // ~ + private final ReadOnlyBooleanWrapper sourceCodeToggle = new ReadOnlyBooleanWrapper(); + public ReadOnlyBooleanProperty sourceCodeToggleProperty() { return sourceCodeToggle.getReadOnlyProperty(); } + // ~ + private final ReadOnlyObjectWrapper> selectedPage = new ReadOnlyObjectWrapper<>(); + public ReadOnlyObjectProperty> selectedPageProperty() { return selectedPage.getReadOnlyProperty(); } + // ~ + private final ReadOnlyObjectWrapper currentSubLayer = new ReadOnlyObjectWrapper<>(PAGE); + public ReadOnlyObjectProperty currentSubLayerProperty() { return currentSubLayer.getReadOnlyProperty(); } + // ~ + private final ReadOnlyObjectWrapper navTree = new ReadOnlyObjectWrapper<>(createTree()); + + public ReadOnlyObjectProperty 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, NavTree.Item> createNavItems() { + var map = new HashMap, 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 // /////////////////////////////////////////////////////////////////////////// diff --git a/sampler/src/main/java/atlantafx/sampler/layout/Nav.java b/sampler/src/main/java/atlantafx/sampler/layout/Nav.java new file mode 100644 index 0000000..e70cc67 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/layout/Nav.java @@ -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 pageClass, + @Nullable List 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()); + } +} \ No newline at end of file diff --git a/sampler/src/main/java/atlantafx/sampler/layout/NavTree.java b/sampler/src/main/java/atlantafx/sampler/layout/NavTree.java new file mode 100644 index 0000000..17125b2 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/layout/NavTree.java @@ -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