Rewrite Breadcrumbs for more customization
This is breaking change which also significantly improves control API. It introduces divider factory and makes possible to use hyperlinks as control items.
This commit is contained in:
parent
71ea3aac8b
commit
2977aaca66
@ -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.
|
||||
* <p>
|
||||
* 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<T> 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<TE> extends Event {
|
||||
protected final Callback<BreadCrumbItem<T>, ButtonBase> defaultCrumbNodeFactory =
|
||||
item -> new Hyperlink(item.getStringValue());
|
||||
protected final Callback<BreadCrumbItem<T>, ? 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<BreadCrumbActionEvent<?>> CRUMB_ACTION
|
||||
= new EventType<>("CRUMB_ACTION" + UUID.randomUUID());
|
||||
/** Creates an empty bread crumb bar. */
|
||||
public Breadcrumbs() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
private final TreeItem<TE> selectedCrumb;
|
||||
/**
|
||||
* Creates a bread crumb bar with the given BreadCrumbItem as the currently
|
||||
* selected crumb.
|
||||
*/
|
||||
public Breadcrumbs(BreadCrumbItem<T> selectedCrumb) {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
/** Creates a new event that can subsequently be fired. */
|
||||
public BreadCrumbActionEvent(TreeItem<TE> 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<TE> 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<T> extends Control {
|
||||
* as selectedCrumb node to be shown
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static <T> TreeItem<T> buildTreeModel(T... crumbs) {
|
||||
TreeItem<T> subRoot = null;
|
||||
public static <T> BreadCrumbItem<T> buildTreeModel(T... crumbs) {
|
||||
BreadCrumbItem<T> subRoot = null;
|
||||
for (T crumb : crumbs) {
|
||||
TreeItem<T> currentNode = new TreeItem<>(crumb);
|
||||
BreadCrumbItem<T> currentNode = new BreadCrumbItem<>(crumb);
|
||||
if (subRoot != null) {
|
||||
subRoot.getChildren().add(currentNode);
|
||||
}
|
||||
@ -95,24 +103,9 @@ public class Breadcrumbs<T> extends Control {
|
||||
return subRoot;
|
||||
}
|
||||
|
||||
/** Default crumb node factory. This factory is used when no custom factory is specified by the user. */
|
||||
private final Callback<TreeItem<T>, 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<T> 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<T> extends Control {
|
||||
* <p>
|
||||
* To show the above bread crumb bar, you have to set the [file.txt] tree-node as selected crumb.
|
||||
*/
|
||||
public final ObjectProperty<TreeItem<T>> selectedCrumbProperty() {
|
||||
public final ObjectProperty<BreadCrumbItem<T>> selectedCrumbProperty() {
|
||||
return selectedCrumb;
|
||||
}
|
||||
|
||||
private final ObjectProperty<TreeItem<T>> selectedCrumb =
|
||||
protected final ObjectProperty<BreadCrumbItem<T>> selectedCrumb =
|
||||
new SimpleObjectProperty<>(this, "selectedCrumb");
|
||||
|
||||
/** Get the current target path. */
|
||||
public final TreeItem<T> getSelectedCrumb() {
|
||||
public final BreadCrumbItem<T> getSelectedCrumb() {
|
||||
return selectedCrumb.get();
|
||||
}
|
||||
|
||||
/** Select one node in the BreadCrumbBar for being the bottom-most path node. */
|
||||
public final void setSelectedCrumb(TreeItem<T> selectedCrumb) {
|
||||
public final void setSelectedCrumb(BreadCrumbItem<T> 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<T> 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.
|
||||
* <code>null</code> is not allowed and will result in a fallback to the default factory.
|
||||
* <p>
|
||||
* <code>BreadCrumbItem<T></code> specifies the tree item for creating bread crumb. Use
|
||||
* {@link BreadCrumbItem#isFirst()} and {@link BreadCrumbItem#isLast()} to create bread crumb
|
||||
* depending on item position.
|
||||
* <p>
|
||||
* <code>ButtonBase</code> stands for resulting bread crumb node. It CAN NOT be <code>null</code>.
|
||||
*/
|
||||
public final ObjectProperty<Callback<TreeItem<T>, Button>> crumbFactoryProperty() {
|
||||
public final ObjectProperty<Callback<BreadCrumbItem<T>, ButtonBase>> crumbFactoryProperty() {
|
||||
return crumbFactory;
|
||||
}
|
||||
|
||||
private final ObjectProperty<Callback<TreeItem<T>, Button>> crumbFactory =
|
||||
protected final ObjectProperty<Callback<BreadCrumbItem<T>, ButtonBase>> crumbFactory =
|
||||
new SimpleObjectProperty<>(this, "crumbFactory");
|
||||
|
||||
/**
|
||||
* Sets the crumb factory to create (custom) {@link BreadCrumbButton} instances.
|
||||
* <code>null</code> is not allowed and will result in a fallback to the default factory.
|
||||
*/
|
||||
public final void setCrumbFactory(Callback<TreeItem<T>, Button> value) {
|
||||
public final void setCrumbFactory(Callback<BreadCrumbItem<T>, 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<TreeItem<T>, Button> getCrumbFactory() {
|
||||
public final Callback<BreadCrumbItem<T>, ButtonBase> getCrumbFactory() {
|
||||
return crumbFactory.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an ObjectProperty representing the crumbAction EventHandler being used.
|
||||
* Divider factory is used to create custom divider instances.
|
||||
* <code>null</code> is not allowed and will result in a fallback to the default factory.
|
||||
* <p>
|
||||
* <code>BreadCrumbItem<T></code> 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.
|
||||
* <p>
|
||||
* <code>? extends Node</code> stands for resulting divider node. It CAN be <code>null</code>, which
|
||||
* means there will be no divider inserted after the specified bread crumb.
|
||||
*/
|
||||
public final ObjectProperty<Callback<BreadCrumbItem<T>, ? extends Node>> dividerFactoryProperty() {
|
||||
return dividerFactory;
|
||||
}
|
||||
|
||||
protected final ObjectProperty<Callback<BreadCrumbItem<T>, ? extends Node>> dividerFactory =
|
||||
new SimpleObjectProperty<>(this, "dividerFactory");
|
||||
|
||||
public final void setDividerFactory(Callback<BreadCrumbItem<T>, ? extends Node> value) {
|
||||
if (value == null) {
|
||||
value = defaultDividerFactory;
|
||||
}
|
||||
dividerFactoryProperty().set(value);
|
||||
}
|
||||
|
||||
public final Callback<BreadCrumbItem<T>, ? extends Node> getDividerFactory() {
|
||||
return dividerFactory.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The EventHandler is called when a user selects a bread crumb.
|
||||
*/
|
||||
public final ObjectProperty<EventHandler<BreadCrumbActionEvent<T>>> onCrumbActionProperty() {
|
||||
return onCrumbAction;
|
||||
@ -215,16 +225,11 @@ public class Breadcrumbs<T> 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<BreadCrumbActionEvent<T>> getOnCrumbAction() {
|
||||
return onCrumbActionProperty().get();
|
||||
}
|
||||
|
||||
private final ObjectProperty<EventHandler<BreadCrumbActionEvent<T>>> onCrumbAction = new ObjectPropertyBase<>() {
|
||||
protected final ObjectProperty<EventHandler<BreadCrumbActionEvent<T>>> onCrumbAction = new ObjectPropertyBase<>() {
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Override
|
||||
@ -243,117 +248,60 @@ public class Breadcrumbs<T> 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<T> extends TreeItem<T> {
|
||||
|
||||
// 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<Boolean> 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<TE> 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<BreadCrumbActionEvent<?>> CRUMB_ACTION
|
||||
= new EventType<>("CRUMB_ACTION" + UUID.randomUUID());
|
||||
|
||||
private final BreadCrumbItem<TE> selectedCrumb;
|
||||
|
||||
/** Creates a new event that can subsequently be fired. */
|
||||
public BreadCrumbActionEvent(BreadCrumbItem<TE> 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<TE> getSelectedCrumb() {
|
||||
return selectedCrumb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<T> extends SkinBase<Breadcrumbs<T>> {
|
||||
|
||||
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<TreeModificationEvent<Object>> treeChildrenModifiedHandler = e -> updateBreadCrumbs();
|
||||
|
||||
public BreadcrumbsSkin(final Breadcrumbs<T> 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<TreeItem<T>> 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<T> newTarget, TreeItem<T> 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<TreeModificationEvent<Object>> treeChildrenModifiedHandler =
|
||||
args -> updateBreadCrumbs();
|
||||
double nodeWidth = snapSizeX(node.prefWidth(height));
|
||||
double nodeHeight = snapSizeY(node.prefHeight(-1));
|
||||
|
||||
private void updateBreadCrumbs() {
|
||||
final Breadcrumbs<T> buttonBar = getSkinnable();
|
||||
final TreeItem<T> pathTarget = buttonBar.getSelectedCrumb();
|
||||
final Callback<TreeItem<T>, Button> factory = buttonBar.getCrumbFactory();
|
||||
// center node within the breadcrumbs
|
||||
nodeY = nodeHeight < controlHeight ? (controlHeight - nodeHeight) / 2 : y;
|
||||
|
||||
getChildren().clear();
|
||||
|
||||
if (pathTarget != null) {
|
||||
List<TreeItem<T>> 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<T> old, BreadCrumbItem<T> val) {
|
||||
if (old != null) {
|
||||
old.removeEventHandler(BreadCrumbItem.childrenModificationEvent(), treeChildrenModifiedHandler);
|
||||
}
|
||||
if (val != null) {
|
||||
val.addEventHandler(BreadCrumbItem.childrenModificationEvent(), treeChildrenModifiedHandler);
|
||||
}
|
||||
updateBreadCrumbs();
|
||||
}
|
||||
|
||||
protected void updateBreadCrumbs() {
|
||||
getChildren().clear();
|
||||
BreadCrumbItem<T> 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<BreadCrumbItem<T>> crumbs = constructFlatPath(selectedTreeItem);
|
||||
for (BreadCrumbItem<T> 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<T> extends SkinBase<Breadcrumbs<T>> {
|
||||
*
|
||||
* @param bottomMost The crumb node at the end of the path
|
||||
*/
|
||||
private List<TreeItem<T>> constructFlatPath(TreeItem<T> bottomMost) {
|
||||
List<TreeItem<T>> path = new ArrayList<>();
|
||||
protected List<BreadCrumbItem<T>> constructFlatPath(BreadCrumbItem<T> bottomMost) {
|
||||
List<BreadCrumbItem<T>> path = new ArrayList<>();
|
||||
|
||||
TreeItem<T> current = bottomMost;
|
||||
BreadCrumbItem<T> current = bottomMost;
|
||||
do {
|
||||
path.add(current);
|
||||
current = current.getParent();
|
||||
current.setFirst(false);
|
||||
current.setLast(false);
|
||||
current = (BreadCrumbItem<T>) 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<TreeItem<T>, Button> factory,
|
||||
final TreeItem<T> selectedCrumb) {
|
||||
protected ButtonBase createCrumb(BreadCrumbItem<T> 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<T> 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<T> crumbModel) {
|
||||
protected void onBreadCrumbAction(BreadCrumbItem<T> crumbModel) {
|
||||
final Breadcrumbs<T> breadCrumbBar = getSkinnable();
|
||||
|
||||
// fire the composite event in the breadCrumbBar
|
||||
|
@ -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<TreeItem<String>, Button> crumbFactory = crumb -> {
|
||||
var btn = new Button(crumb.getValue());
|
||||
Callback<BreadCrumbItem<String>, 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<TreeItem<String>, Button> crumbFactory) {
|
||||
int count = 5;
|
||||
TreeItem<String> model = Breadcrumbs.buildTreeModel(
|
||||
generate(() -> FAKER.science().element(), count).toArray(String[]::new)
|
||||
private SampleBlock customDividerSample() {
|
||||
Callback<BreadCrumbItem<String>, ? 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<BreadCrumbItem<String>, ButtonBase> crumbFactory,
|
||||
Callback<BreadCrumbItem<String>, ? extends Node> dividerFactory) {
|
||||
BreadCrumbItem<String> 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<String> selected = breadcrumbs.getSelectedCrumb();
|
||||
BreadCrumbItem<String> selected = breadcrumbs.getSelectedCrumb();
|
||||
if (selected.getChildren().size() > 0) {
|
||||
breadcrumbs.setSelectedCrumb(selected.getChildren().get(0));
|
||||
breadcrumbs.setSelectedCrumb((BreadCrumbItem<String>) selected.getChildren().get(0));
|
||||
}
|
||||
});
|
||||
|
||||
@ -80,13 +94,19 @@ public class BreadcrumbsPage extends AbstractPage {
|
||||
return box;
|
||||
}
|
||||
|
||||
private <T> TreeItem<T> getAncestor(TreeItem<T> node, int height) {
|
||||
private <T> BreadCrumbItem<T> getAncestor(BreadCrumbItem<T> node, int height) {
|
||||
var counter = height;
|
||||
var current = node;
|
||||
while (counter > 0 && current.getParent() != null) {
|
||||
current = current.getParent();
|
||||
current = (BreadCrumbItem<T>) current.getParent();
|
||||
counter--;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width) {
|
||||
System.out.println("pref height" + super.computePrefHeight(width));
|
||||
return super.computePrefHeight(width);
|
||||
}
|
||||
}
|
||||
|
@ -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<Path> breadCrumbs() {
|
||||
Callback<TreeItem<Path>, Button> crumbFactory = crumb -> {
|
||||
Callback<BreadCrumbItem<Path>, 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<BreadCrumbItem<Path>, ? extends Node> dividerFactory = item ->
|
||||
item != null && !item.isLast() ? new Label("", new FontIcon(Material2AL.CHEVRON_RIGHT)) : null;
|
||||
|
||||
var breadcrumbs = new Breadcrumbs<Path>();
|
||||
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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user