diff --git a/base/src/main/java/atlantafx/base/controls/Breadcrumbs.java b/base/src/main/java/atlantafx/base/controls/Breadcrumbs.java index 9be791f..8047775 100755 --- a/base/src/main/java/atlantafx/base/controls/Breadcrumbs.java +++ b/base/src/main/java/atlantafx/base/controls/Breadcrumbs.java @@ -28,18 +28,13 @@ */ package atlantafx.base.controls; -import javafx.beans.InvalidationListener; import javafx.beans.property.*; import javafx.event.Event; import javafx.event.EventHandler; import javafx.event.EventType; import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.Control; -import javafx.scene.control.Skin; -import javafx.scene.control.TreeItem; -import javafx.scene.paint.Color; -import javafx.scene.shape.*; +import javafx.scene.control.*; +import javafx.scene.layout.Region; import javafx.util.Callback; import java.util.UUID; @@ -47,35 +42,48 @@ import java.util.UUID; /** * Represents a bread crumb bar. This control is useful to visualize and navigate * a hierarchical path structure, such as file systems. + *

+ * A breadcrumbs consist of two types of elements: a button (default is a hyperlink) + * and a divider (default is for Label). You can customize both by providing the + * corresponding control factory. */ @SuppressWarnings("unused") public class Breadcrumbs extends Control { - private static final String STYLE_CLASS_FIRST = "first"; + protected static final String DEFAULT_STYLE_CLASS = "breadcrumbs"; - /** Represents an Event which is fired when a bread crumb was activated. */ - public static class BreadCrumbActionEvent extends Event { + protected final Callback, ButtonBase> defaultCrumbNodeFactory = + item -> new Hyperlink(item.getStringValue()); + protected final Callback, ? extends Node> defaultDividerFactory = + item -> item != null && !item.isLast() ? new Label("/") : null; - /** - * The event type that should be listened to by people interested in - * knowing when the {@link Breadcrumbs#selectedCrumbProperty() selected crumb} - * has changed. - */ - public static final EventType> CRUMB_ACTION - = new EventType<>("CRUMB_ACTION" + UUID.randomUUID()); + /** Creates an empty bread crumb bar. */ + public Breadcrumbs() { + this(null); + } - private final TreeItem selectedCrumb; + /** + * Creates a bread crumb bar with the given BreadCrumbItem as the currently + * selected crumb. + */ + public Breadcrumbs(BreadCrumbItem selectedCrumb) { + getStyleClass().add(DEFAULT_STYLE_CLASS); - /** Creates a new event that can subsequently be fired. */ - public BreadCrumbActionEvent(TreeItem selectedCrumb) { - super(CRUMB_ACTION); - this.selectedCrumb = selectedCrumb; - } + // breadcrumbs should be the size of its content + setPrefWidth(Region.USE_COMPUTED_SIZE); + setMaxWidth(Region.USE_PREF_SIZE); + setPrefHeight(Region.USE_COMPUTED_SIZE); + setMaxHeight(Region.USE_PREF_SIZE); - /** Returns the crumb which was the action target. */ - public TreeItem getSelectedCrumb() { - return selectedCrumb; - } + setSelectedCrumb(selectedCrumb); + setCrumbFactory(defaultCrumbNodeFactory); + setDividerFactory(defaultDividerFactory); + } + + /** {@inheritDoc} */ + @Override + protected Skin createDefaultSkin() { + return new BreadcrumbsSkin<>(this); } /** @@ -83,10 +91,10 @@ public class Breadcrumbs extends Control { * as selectedCrumb node to be shown */ @SafeVarargs - public static TreeItem buildTreeModel(T... crumbs) { - TreeItem subRoot = null; + public static BreadCrumbItem buildTreeModel(T... crumbs) { + BreadCrumbItem subRoot = null; for (T crumb : crumbs) { - TreeItem currentNode = new TreeItem<>(crumb); + BreadCrumbItem currentNode = new BreadCrumbItem<>(crumb); if (subRoot != null) { subRoot.getChildren().add(currentNode); } @@ -95,24 +103,9 @@ public class Breadcrumbs extends Control { return subRoot; } - /** Default crumb node factory. This factory is used when no custom factory is specified by the user. */ - private final Callback, Button> defaultCrumbNodeFactory = - crumb -> new BreadCrumbButton(crumb.getValue() != null ? crumb.getValue().toString() : ""); - - /** Creates an empty bread crumb bar. */ - public Breadcrumbs() { - this(null); - } - - /** - * Creates a bread crumb bar with the given TreeItem as the currently - * selected crumb. - */ - public Breadcrumbs(TreeItem selectedCrumb) { - getStyleClass().add(DEFAULT_STYLE_CLASS); - setSelectedCrumb(selectedCrumb); - setCrumbFactory(defaultCrumbNodeFactory); - } + /////////////////////////////////////////////////////////////////////////// + // Properties // + /////////////////////////////////////////////////////////////////////////// /** * Represents the bottom-most path node (the node on the most-right side in @@ -125,26 +118,25 @@ public class Breadcrumbs extends Control { *

* To show the above bread crumb bar, you have to set the [file.txt] tree-node as selected crumb. */ - public final ObjectProperty> selectedCrumbProperty() { + public final ObjectProperty> selectedCrumbProperty() { return selectedCrumb; } - private final ObjectProperty> selectedCrumb = + protected final ObjectProperty> selectedCrumb = new SimpleObjectProperty<>(this, "selectedCrumb"); - /** Get the current target path. */ - public final TreeItem getSelectedCrumb() { + public final BreadCrumbItem getSelectedCrumb() { return selectedCrumb.get(); } - /** Select one node in the BreadCrumbBar for being the bottom-most path node. */ - public final void setSelectedCrumb(TreeItem selectedCrumb) { + public final void setSelectedCrumb(BreadCrumbItem selectedCrumb) { this.selectedCrumb.set(selectedCrumb); } /** - * Enable or disable auto navigation (default is enabled). - * If auto navigation is enabled, it will automatically navigate to the crumb which was clicked by the user. + * Enables or disables auto navigation (default is enabled). + * If auto navigation is enabled, it will automatically navigate to the crumb which was + * clicked by the user. * * @return a {@link BooleanProperty} */ @@ -152,59 +144,77 @@ public class Breadcrumbs extends Control { return autoNavigation; } - private final BooleanProperty autoNavigation = + protected final BooleanProperty autoNavigation = new SimpleBooleanProperty(this, "autoNavigationEnabled", true); - /** - * Return whether auto-navigation is enabled. - * - * @return whether auto-navigation is enabled. - */ public final boolean isAutoNavigationEnabled() { return autoNavigation.get(); } - /** - * Enable or disable auto navigation (default is enabled). - * If auto navigation is enabled, it will automatically navigate to the crumb which was clicked by the user. - */ public final void setAutoNavigationEnabled(boolean enabled) { autoNavigation.set(enabled); } /** - * Return an ObjectProperty of the CrumbFactory. - * - * @return an ObjectProperty of the CrumbFactory. + * Crumb factory is used to create custom bread crumb instances. + * null is not allowed and will result in a fallback to the default factory. + *

+ * BreadCrumbItem specifies the tree item for creating bread crumb. Use + * {@link BreadCrumbItem#isFirst()} and {@link BreadCrumbItem#isLast()} to create bread crumb + * depending on item position. + *

+ * ButtonBase stands for resulting bread crumb node. It CAN NOT be null. */ - public final ObjectProperty, Button>> crumbFactoryProperty() { + public final ObjectProperty, ButtonBase>> crumbFactoryProperty() { return crumbFactory; } - private final ObjectProperty, Button>> crumbFactory = + protected final ObjectProperty, ButtonBase>> crumbFactory = new SimpleObjectProperty<>(this, "crumbFactory"); - /** - * Sets the crumb factory to create (custom) {@link BreadCrumbButton} instances. - * null is not allowed and will result in a fallback to the default factory. - */ - public final void setCrumbFactory(Callback, Button> value) { + public final void setCrumbFactory(Callback, ButtonBase> value) { if (value == null) { value = defaultCrumbNodeFactory; } crumbFactoryProperty().set(value); } - /** - * Returns the cell factory that will be used to create {@link BreadCrumbButton} - * instances - */ - public final Callback, Button> getCrumbFactory() { + public final Callback, ButtonBase> getCrumbFactory() { return crumbFactory.get(); } /** - * @return an ObjectProperty representing the crumbAction EventHandler being used. + * Divider factory is used to create custom divider instances. + * null is not allowed and will result in a fallback to the default factory. + *

+ * BreadCrumbItem specifies the preceding tree item. It can be null, because this way + * you can insert divider before the first bread crumb, which can be used e.g. for creating a Unix path. + * Use {@link BreadCrumbItem#isFirst()} and {@link BreadCrumbItem#isLast()} to create divider + * depending on item position. + *

+ * ? extends Node stands for resulting divider node. It CAN be null, which + * means there will be no divider inserted after the specified bread crumb. + */ + public final ObjectProperty, ? extends Node>> dividerFactoryProperty() { + return dividerFactory; + } + + protected final ObjectProperty, ? extends Node>> dividerFactory = + new SimpleObjectProperty<>(this, "dividerFactory"); + + public final void setDividerFactory(Callback, ? extends Node> value) { + if (value == null) { + value = defaultDividerFactory; + } + dividerFactoryProperty().set(value); + } + + public final Callback, ? extends Node> getDividerFactory() { + return dividerFactory.get(); + } + + /** + * The EventHandler is called when a user selects a bread crumb. */ public final ObjectProperty>> onCrumbActionProperty() { return onCrumbAction; @@ -215,16 +225,11 @@ public class Breadcrumbs extends Control { onCrumbActionProperty().set(value); } - /** - * Return the EventHandler currently used when a user selects a crumb. - * - * @return the EventHandler currently used when a user selects a crumb. - */ public final EventHandler> getOnCrumbAction() { return onCrumbActionProperty().get(); } - private final ObjectProperty>> onCrumbAction = new ObjectPropertyBase<>() { + protected final ObjectProperty>> onCrumbAction = new ObjectPropertyBase<>() { @SuppressWarnings({ "unchecked", "rawtypes" }) @Override @@ -243,117 +248,60 @@ public class Breadcrumbs extends Control { } }; - private static final String DEFAULT_STYLE_CLASS = "bread-crumb-bar"; + /////////////////////////////////////////////////////////////////////////// - /** {@inheritDoc} */ - @Override - protected Skin createDefaultSkin() { - return new BreadcrumbsSkin<>(this); + public static class BreadCrumbItem extends TreeItem { + + // setters must not be exposed to user + private boolean first; + private boolean last; + + public BreadCrumbItem(T value) { + super(value); + } + + public boolean isFirst() { + return first; + } + + void setFirst(boolean first) { + this.first = first; + } + + public boolean isLast() { + return last; + } + + void setLast(boolean last) { + this.last = last; + } + + String getStringValue() { + return getValue() != null ? getValue().toString() : ""; + } } - @SuppressWarnings("FieldCanBeLocal") - public static class BreadCrumbButton extends Button { - - private final ObjectProperty first = new SimpleObjectProperty<>(this, STYLE_CLASS_FIRST); - - private final double arrowWidth = 5; - private final double arrowHeight = 20; + /** Represents an Event which is fired when a bread crumb was activated. */ + public static class BreadCrumbActionEvent extends Event { /** - * Create a BreadCrumbButton - * - * @param text Buttons text + * The event type that should be listened to by people interested in + * knowing when the {@link Breadcrumbs#selectedCrumbProperty() selected crumb} + * has changed. */ - public BreadCrumbButton(String text) { - this(text, null); + public static final EventType> CRUMB_ACTION + = new EventType<>("CRUMB_ACTION" + UUID.randomUUID()); + + private final BreadCrumbItem selectedCrumb; + + /** Creates a new event that can subsequently be fired. */ + public BreadCrumbActionEvent(BreadCrumbItem selectedCrumb) { + super(CRUMB_ACTION); + this.selectedCrumb = selectedCrumb; } - /** - * Create a BreadCrumbButton - * - * @param text Buttons text - * @param gfx Gfx of the Button - */ - public BreadCrumbButton(String text, Node gfx) { - super(text, gfx); - first.set(false); - - getStyleClass().addListener((InvalidationListener) obs -> updateShape()); - - updateShape(); - } - - private void updateShape() { - this.setShape(createButtonShape()); - } - - /** Returns the crumb arrow width. */ - public double getArrowWidth() { - return arrowWidth; - } - - /** Creates an arrow path. */ - private Path createButtonShape() { - // build the following shape (or home without left arrow) - - // -------- - // \ \ - // / / - // -------- - Path path = new Path(); - - // begin in the upper left corner - MoveTo e1 = new MoveTo(0, 0); - path.getElements().add(e1); - - // draw a horizontal line that defines the width of the shape - HLineTo e2 = new HLineTo(); - // bind the width of the shape to the width of the button - e2.xProperty().bind(this.widthProperty().subtract(arrowWidth)); - path.getElements().add(e2); - - // draw upper part of right arrow - LineTo e3 = new LineTo(); - // the x endpoint of this line depends on the x property of line e2 - e3.xProperty().bind(e2.xProperty().add(arrowWidth)); - e3.setY(arrowHeight / 2.0); - path.getElements().add(e3); - - // draw lower part of right arrow - LineTo e4 = new LineTo(); - // the x endpoint of this line depends on the x property of line e2 - e4.xProperty().bind(e2.xProperty()); - e4.setY(arrowHeight); - path.getElements().add(e4); - - // draw lower horizontal line - HLineTo e5 = new HLineTo(0); - path.getElements().add(e5); - - if (!getStyleClass().contains(STYLE_CLASS_FIRST)) { - // draw lower part of left arrow - // we simply can omit it for the first Button - LineTo e6 = new LineTo(arrowWidth, arrowHeight / 2.0); - path.getElements().add(e6); - } else { - // draw an arc for the first bread crumb - ArcTo arcTo = new ArcTo(); - arcTo.setSweepFlag(true); - arcTo.setX(0); - arcTo.setY(0); - arcTo.setRadiusX(15.0f); - arcTo.setRadiusY(15.0f); - path.getElements().add(arcTo); - } - - // close path - ClosePath e7 = new ClosePath(); - path.getElements().add(e7); - - // this is a dummy color to fill the shape, it won't be visible - path.setFill(Color.BLACK); - - return path; + public BreadCrumbItem getSelectedCrumb() { + return selectedCrumb; } } } diff --git a/base/src/main/java/atlantafx/base/controls/BreadcrumbsSkin.java b/base/src/main/java/atlantafx/base/controls/BreadcrumbsSkin.java index aed37ef..63c67e4 100755 --- a/base/src/main/java/atlantafx/base/controls/BreadcrumbsSkin.java +++ b/base/src/main/java/atlantafx/base/controls/BreadcrumbsSkin.java @@ -28,15 +28,14 @@ */ package atlantafx.base.controls; -import javafx.beans.value.ChangeListener; +import atlantafx.base.controls.Breadcrumbs.BreadCrumbItem; +import javafx.css.PseudoClass; import javafx.event.Event; import javafx.event.EventHandler; import javafx.scene.Node; -import javafx.scene.control.Button; +import javafx.scene.control.ButtonBase; import javafx.scene.control.SkinBase; -import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem.TreeModificationEvent; -import javafx.util.Callback; import java.util.ArrayList; import java.util.Collections; @@ -44,89 +43,90 @@ import java.util.List; public class BreadcrumbsSkin extends SkinBase> { - private static final String STYLE_CLASS_FIRST = "first"; - private static final String STYLE_CLASS_LAST = "last"; + protected static final PseudoClass FIRST = PseudoClass.getPseudoClass("first"); + protected static final PseudoClass LAST = PseudoClass.getPseudoClass("last"); + + protected final EventHandler> treeChildrenModifiedHandler = e -> updateBreadCrumbs(); public BreadcrumbsSkin(final Breadcrumbs control) { super(control); - control.selectedCrumbProperty().addListener(selectedPathChangeListener); + + control.selectedCrumbProperty().addListener((obs, old, val) -> updateSelectedPath(old, val)); updateSelectedPath(getSkinnable().selectedCrumbProperty().get(), null); } - @SuppressWarnings("FieldCanBeLocal") - private final ChangeListener> selectedPathChangeListener = - (obs, oldItem, newItem) -> updateSelectedPath(newItem, oldItem); + @Override + protected void layoutChildren(double x, double y, double width, double height) { + double controlHeight = getSkinnable().getHeight(); + double nodeX = x, nodeY; - private void updateSelectedPath(TreeItem newTarget, TreeItem oldTarget) { - if (oldTarget != null) { - // remove old listener - oldTarget.removeEventHandler(TreeItem.childrenModificationEvent(), treeChildrenModifiedHandler); - } - if (newTarget != null) { - // add new listener - newTarget.addEventHandler(TreeItem.childrenModificationEvent(), treeChildrenModifiedHandler); - } - updateBreadCrumbs(); - } + for (int i = 0; i < getChildren().size(); i++) { + Node node = getChildren().get(i); - private final EventHandler> treeChildrenModifiedHandler = - args -> updateBreadCrumbs(); + double nodeWidth = snapSizeX(node.prefWidth(height)); + double nodeHeight = snapSizeY(node.prefHeight(-1)); - private void updateBreadCrumbs() { - final Breadcrumbs buttonBar = getSkinnable(); - final TreeItem pathTarget = buttonBar.getSelectedCrumb(); - final Callback, Button> factory = buttonBar.getCrumbFactory(); + // center node within the breadcrumbs + nodeY = nodeHeight < controlHeight ? (controlHeight - nodeHeight) / 2 : y; - getChildren().clear(); - - if (pathTarget != null) { - List> crumbs = constructFlatPath(pathTarget); - - for (int i = 0; i < crumbs.size(); i++) { - Button crumb = createCrumb(factory, crumbs.get(i)); - crumb.setMnemonicParsing(false); - - boolean first = crumbs.size() > 1 && i == 0; - boolean last = crumbs.size() > 1 && i == crumbs.size() - 1; - - if (first) { - crumb.getStyleClass().remove(STYLE_CLASS_LAST); - addStyleClass(crumb, STYLE_CLASS_FIRST); - } else if (last) { - crumb.getStyleClass().remove(STYLE_CLASS_FIRST); - addStyleClass(crumb, STYLE_CLASS_LAST); - } else { - crumb.getStyleClass().removeAll(STYLE_CLASS_FIRST, STYLE_CLASS_LAST); - } - - getChildren().add(crumb); - } - } - } - - private void addStyleClass(Node node, String styleClass) { - if (!node.getStyleClass().contains(styleClass)) { - node.getStyleClass().add(styleClass); + node.resizeRelocate(nodeX, nodeY, nodeWidth, nodeHeight); + nodeX += nodeWidth; } } @Override - protected void layoutChildren(double x, double y, double w, double h) { - for (int i = 0; i < getChildren().size(); i++) { - Node n = getChildren().get(i); + protected double computeMinWidth(double height, double topInset, double rightInset, + double bottomInset, double leftInset) { + double width = 0; + for (Node node : getChildren()) { + if (!node.isManaged()) { continue; } + width += snapSizeX(node.prefWidth(height)); + } - double nw = snapSizeX(n.prefWidth(h)); - double nh = snapSizeY(n.prefHeight(-1)); + return width + rightInset + leftInset; + } - if (i > 0) { - // we have to position the bread crumbs slightly overlapping - double ins = n instanceof Breadcrumbs.BreadCrumbButton d ? d.getArrowWidth() : 0; - x = snapPositionX(x - ins); + protected void updateSelectedPath(BreadCrumbItem old, BreadCrumbItem val) { + if (old != null) { + old.removeEventHandler(BreadCrumbItem.childrenModificationEvent(), treeChildrenModifiedHandler); + } + if (val != null) { + val.addEventHandler(BreadCrumbItem.childrenModificationEvent(), treeChildrenModifiedHandler); + } + updateBreadCrumbs(); + } + + protected void updateBreadCrumbs() { + getChildren().clear(); + BreadCrumbItem selectedTreeItem = getSkinnable().getSelectedCrumb(); + Node divider; + if (selectedTreeItem != null) { + // optionally insert divider before the first node + divider = createDivider(null); + if (divider != null) { + divider.pseudoClassStateChanged(FIRST, true); + divider.pseudoClassStateChanged(LAST, false); + getChildren().add(divider); } - n.resize(nw, nh); - n.relocate(x, y); - x += nw; + List> crumbs = constructFlatPath(selectedTreeItem); + for (BreadCrumbItem treeItem : crumbs) { + ButtonBase crumb = createCrumb(treeItem); + crumb.pseudoClassStateChanged(FIRST, treeItem.isFirst()); + crumb.pseudoClassStateChanged(LAST, treeItem.isLast()); + getChildren().add(crumb); + + // for the sake of flexibility, it's user responsibility to decide + // whether insert divider after the last node or not + divider = createDivider(treeItem); + if (divider != null) { + if (treeItem.isLast()) { + divider.pseudoClassStateChanged(FIRST, false); + divider.pseudoClassStateChanged(LAST, true); + } + getChildren().add(divider); + } + } } } @@ -135,39 +135,59 @@ public class BreadcrumbsSkin extends SkinBase> { * * @param bottomMost The crumb node at the end of the path */ - private List> constructFlatPath(TreeItem bottomMost) { - List> path = new ArrayList<>(); + protected List> constructFlatPath(BreadCrumbItem bottomMost) { + List> path = new ArrayList<>(); - TreeItem current = bottomMost; + BreadCrumbItem current = bottomMost; do { path.add(current); - current = current.getParent(); + current.setFirst(false); + current.setLast(false); + current = (BreadCrumbItem) current.getParent(); } while (current != null); Collections.reverse(path); + + // if the path consists of a single item it considered as first, but not last + if (path.size() > 0) { path.get(0).setFirst(true); } + if (path.size() > 1) { path.get(path.size() - 1).setLast(true); } + return path; } - private Button createCrumb( - final Callback, Button> factory, - final TreeItem selectedCrumb) { + protected ButtonBase createCrumb(BreadCrumbItem treeItem) { + ButtonBase crumb = getSkinnable().getCrumbFactory().call(treeItem); + crumb.setMnemonicParsing(false); - Button crumb = factory.call(selectedCrumb); - - crumb.getStyleClass().add("crumb"); //$NON-NLS-1$ + if (!crumb.getStyleClass().contains("crumb")) { + crumb.getStyleClass().add("crumb"); + } // listen to the action event of each bread crumb - crumb.setOnAction(ae -> onBreadCrumbAction(selectedCrumb)); + crumb.setOnAction(e -> onBreadCrumbAction(treeItem)); return crumb; } + protected Node createDivider(BreadCrumbItem treeItem) { + Node divider = getSkinnable().getDividerFactory().call(treeItem); + if (divider == null) { + return null; + } + + if (!divider.getStyleClass().contains("divider")) { + divider.getStyleClass().add("divider"); + } + + return divider; + } + /** * Occurs when a bread crumb gets the action event * * @param crumbModel The crumb which received the action event */ - protected void onBreadCrumbAction(final TreeItem crumbModel) { + protected void onBreadCrumbAction(BreadCrumbItem crumbModel) { final Breadcrumbs breadCrumbBar = getSkinnable(); // fire the composite event in the breadCrumbBar diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/BreadcrumbsPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/BreadcrumbsPage.java index 40cdc21..a9aee37 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/components/BreadcrumbsPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/BreadcrumbsPage.java @@ -2,69 +2,83 @@ package atlantafx.sampler.page.components; import atlantafx.base.controls.Breadcrumbs; +import atlantafx.base.controls.Breadcrumbs.BreadCrumbItem; import atlantafx.base.theme.Styles; import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.Page; import atlantafx.sampler.page.SampleBlock; import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.Button; -import javafx.scene.control.TreeItem; +import javafx.scene.control.ButtonBase; +import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.util.Callback; -import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.material2.Material2AL; public class BreadcrumbsPage extends AbstractPage { public static final String NAME = "Breadcrumbs"; + private static final int CRUMB_COUNT = 5; + @Override public String getName() { return NAME; } public BreadcrumbsPage() { super(); - setUserContent(new VBox(Page.PAGE_VGAP, - defaultSample(), - customCrumbSample() + setUserContent(new VBox( + Page.PAGE_VGAP, + basicSample(), + customCrumbSample(), + customDividerSample() )); } - private SampleBlock defaultSample() { - return new SampleBlock("Basic", createBreadcrumbs(null)); + private SampleBlock basicSample() { + return new SampleBlock("Basic", createBreadcrumbs(null, null)); } private SampleBlock customCrumbSample() { - Callback, Button> crumbFactory = crumb -> { - var btn = new Button(crumb.getValue()); + Callback, ButtonBase> crumbFactory = crumb -> { + var btn = new Button(crumb.getValue(), new FontIcon(randomIcon())); btn.getStyleClass().add(Styles.FLAT); btn.setFocusTraversable(false); - if (!crumb.getChildren().isEmpty()) { - btn.setGraphic(new FontIcon(Feather.CHEVRON_RIGHT)); - } return btn; }; - return new SampleBlock("Flat", createBreadcrumbs(crumbFactory)); + return new SampleBlock("Flat Button", createBreadcrumbs(crumbFactory, null)); } - private HBox createBreadcrumbs(Callback, Button> crumbFactory) { - int count = 5; - TreeItem model = Breadcrumbs.buildTreeModel( - generate(() -> FAKER.science().element(), count).toArray(String[]::new) + private SampleBlock customDividerSample() { + Callback, ? extends Node> dividerFactory = item -> { + if (item == null) { return new Label("", new FontIcon(Material2AL.HOME)); } + return !item.isLast() ? new Label("", new FontIcon(Material2AL.CHEVRON_RIGHT)) : null; + }; + + return new SampleBlock("Custom Divider", createBreadcrumbs(null, dividerFactory)); + } + + private HBox createBreadcrumbs(Callback, ButtonBase> crumbFactory, + Callback, ? extends Node> dividerFactory) { + BreadCrumbItem model = Breadcrumbs.buildTreeModel( + generate(() -> FAKER.science().element(), CRUMB_COUNT).toArray(String[]::new) ); var nextBtn = new Button("Next"); - nextBtn.getStyleClass().add(Styles.ACCENT); + nextBtn.getStyleClass().addAll(Styles.ACCENT); var breadcrumbs = new Breadcrumbs<>(model); - breadcrumbs.setSelectedCrumb(getAncestor(model, count / 2)); + breadcrumbs.setSelectedCrumb(getAncestor(model, CRUMB_COUNT / 2)); if (crumbFactory != null) { breadcrumbs.setCrumbFactory(crumbFactory); } + if (dividerFactory != null) { breadcrumbs.setDividerFactory(dividerFactory); } nextBtn.setOnAction(e -> { - TreeItem selected = breadcrumbs.getSelectedCrumb(); + BreadCrumbItem selected = breadcrumbs.getSelectedCrumb(); if (selected.getChildren().size() > 0) { - breadcrumbs.setSelectedCrumb(selected.getChildren().get(0)); + breadcrumbs.setSelectedCrumb((BreadCrumbItem) selected.getChildren().get(0)); } }); @@ -80,13 +94,19 @@ public class BreadcrumbsPage extends AbstractPage { return box; } - private TreeItem getAncestor(TreeItem node, int height) { + private BreadCrumbItem getAncestor(BreadCrumbItem node, int height) { var counter = height; var current = node; while (counter > 0 && current.getParent() != null) { - current = current.getParent(); + current = (BreadCrumbItem) current.getParent(); counter--; } return current; } + + @Override + protected double computePrefHeight(double width) { + System.out.println("pref height" + super.computePrefHeight(width)); + return super.computePrefHeight(width); + } } diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/FileManagerPage.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/FileManagerPage.java index 1ddb417..6e5cbeb 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/FileManagerPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/FileManagerPage.java @@ -2,21 +2,23 @@ package atlantafx.sampler.page.showcase.filemanager; import atlantafx.base.controls.Breadcrumbs; +import atlantafx.base.controls.Breadcrumbs.BreadCrumbItem; import atlantafx.base.controls.Spacer; import atlantafx.base.theme.Tweaks; import atlantafx.sampler.page.showcase.ShowcasePage; import atlantafx.sampler.util.Containers; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.util.Callback; import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.material2.Material2AL; import org.kordamp.ikonli.material2.Material2MZ; import java.net.URI; @@ -156,24 +158,23 @@ public class FileManagerPage extends ShowcasePage { } private Breadcrumbs breadCrumbs() { - Callback, Button> crumbFactory = crumb -> { + Callback, ButtonBase> crumbFactory = crumb -> { var btn = new Button(crumb.getValue().getFileName().toString()); btn.getStyleClass().add(FLAT); btn.setFocusTraversable(false); - if (!crumb.getChildren().isEmpty()) { - btn.setGraphic(new FontIcon(Feather.CHEVRON_RIGHT)); - } return btn; }; + Callback, ? extends Node> dividerFactory = item -> + item != null && !item.isLast() ? new Label("", new FontIcon(Material2AL.CHEVRON_RIGHT)) : null; + var breadcrumbs = new Breadcrumbs(); breadcrumbs.setAutoNavigationEnabled(false); breadcrumbs.setCrumbFactory(crumbFactory); - breadcrumbs.setSelectedCrumb( - Breadcrumbs.buildTreeModel(getParentPath(model.currentPathProperty().get(), 4).toArray(Path[]::new)) + breadcrumbs.setDividerFactory(dividerFactory); + breadcrumbs.setSelectedCrumb(Breadcrumbs.buildTreeModel( + getParentPath(model.currentPathProperty().get(), 4).toArray(Path[]::new)) ); - breadcrumbs.setMaxWidth(Region.USE_PREF_SIZE); - breadcrumbs.setMaxHeight(Region.USE_PREF_SIZE); return breadcrumbs; } diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/file-manager.css b/sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/file-manager.css index ee136b5..17e4ba1 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/file-manager.css +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/file-manager.css @@ -11,6 +11,10 @@ -fx-background-insets: 0, 1; } +.file-manager-showcase .breadcrumbs >.divider { + -fx-padding: 0; +} + .file-manager-showcase .table-directory-view .table-view { -color-header-bg: -color-bg-default; -color-cell-bg-selected: -color-accent-emphasis; diff --git a/styles/src/components/_breadcrumbs.scss b/styles/src/components/_breadcrumbs.scss index c9a7415..f7aad42 100755 --- a/styles/src/components/_breadcrumbs.scss +++ b/styles/src/components/_breadcrumbs.scss @@ -2,36 +2,27 @@ @use "../settings/config" as cfg; -// node hierarchy -// .bread-crumb-bar { -// >.crumb { ... } -// } +$padding-x: cfg.$padding-x !default; +$padding-y: cfg.$padding-y !default; -// right gap is small, because we have crumbs divider on the right, -// which gives us additional padding, at least visually. -$crumb-right-gap: 2px !default; -$crumb-left-gap: calc(cfg.$graphic-gap * 1.5) !default; +$divider-spacing: 0.5em !default; -.bread-crumb-bar { - >.button.flat { - -fx-padding: cfg.$padding-y $crumb-right-gap cfg.$padding-y $crumb-left-gap; - -fx-content-display: RIGHT; +/** +== Structure == +.breadcrumbs { + >.crumb[:first|:last] { ... } + >.divider { ... } +} +*/ - &.first { - -fx-padding: cfg.$padding-y $crumb-right-gap cfg.$padding-y cfg.$padding-x; - } +.breadcrumbs { + -fx-padding: $padding-y $padding-x $padding-y $padding-x; - &.last { - -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y $crumb-left-gap; + >.hyperlink { + -color-link-fg-visited: -color-link-fg; + } - #{cfg.$font-icon-selector} { - // hiding font icon doesn't work as it should, - // and have to be replaced by '-fx-managed' when supported - -fx-min-width: 0; - -fx-pref-width: 0; - -fx-max-width: 0; - visibility: hidden; - } - } + >.label.divider { + -fx-padding: 0 $divider-spacing 0 $divider-spacing; } }