I can trivially create a scroll pane in JavaFX that only scrolls horizontally like so:
ScrollPane scroll = new ScrollPane(lcp);
scroll.setPannable(true);
scroll.setFitToHeight(true);
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
However, the mouse scroll wheel still tries to scroll vertically rather than horizontally in this case (unless I specifically scroll over the horizontal scroll bar.)
How can I set up the scroll pane so that the mouse wheel pans horizontally?
Here is the example application that I wrote for you and does exactly what you want:
public class Test extends Application {
ScrollPane scrollPane;
int pos = 0;
final int minPos = 0;
final int maxPos = 100;
#Override
public void start(Stage primaryStage) {
Label label = new Label("TEXT!!!!!!!TEXT!!!!!!!TEXT!!!!!!!TEXT!!!!!!!TEXT!!!!!!!TEXT");
label.setPrefSize(500, 100);
label.setOnScroll(new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
if (event.getDeltaY() > 0)
scrollPane.setHvalue(pos == minPos ? minPos : pos--);
else
scrollPane.setHvalue(pos == maxPos ? maxPos : pos++);
}
});
scrollPane = new ScrollPane();
scrollPane.setHmin(minPos);
scrollPane.setHmax(maxPos);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setHbarPolicy(ScrollBarPolicy.ALWAYS);
scrollPane.setPannable(true);
scrollPane.setFitToHeight(true);
scrollPane.setContent(label);
BorderPane root = new BorderPane();
root.setPrefSize(200, 100);
root.setCenter(scrollPane);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I have also been looking for a solution and found this one from Ugurcan Yildirim but didn't like the fact that the natural scroll bar length and speed is modified also. This one worked for me:
scrollPane.setOnScroll(event -> {
if(event.getDeltaX() == 0 && event.getDeltaY() != 0) {
scrollPane.setHvalue(scrollPane.getHvalue() - event.getDeltaY() / this.allComments.getWidth());
}
});
event.getDeltaX() == 0 just to be sure that the user is only using the mouse wheel and nothing is adding up
this.allComments is the content of the scrollPane (a HBox in my case). By dividing the delta y value by it's content width the scroll speed is natural according to the amount of content to scroll.
Related
The current implementation of the VirtualFlow only makes scrollbars visible when view rect becomes less than control size. By control I mean ListView, TreeView and whatever standard virtualized controls. The problem is that vertical scrollbar appearance causes recalculation of the control width, namely it slightly shifts cell content to the left side. This is clearly noticeable and very uncomfortable movement.
I need to reserve some space for the vertical scrollbar beforehand, but none of controls provide API to manipulate VirtualFlow scrollbars behavior, which is very unfortunate API design. Not to mention that most of the implementations place scrollbars on top of the component, thus just overlapping the small part of it.
The question is, "Which is the best way to achieve this?". Paddings won't help, and JavaFX has no margins support. I could put control (e.g ListView) inside of ScrollPane, but I'd bet VirtualFlow won't continue to reuse cells in that case, so it's not a solution.
EXAMPLE:
Expand and collapse node2, it shifts lbRight content.
public class Launcher extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
TreeItem<UUID> root = new TreeItem<>(UUID.randomUUID());
TreeView<UUID> tree = new TreeView<>(root);
tree.setCellFactory(list -> new CustomCell());
TreeItem<UUID> node0 = new TreeItem<>(UUID.randomUUID());
TreeItem<UUID> node1 = new TreeItem<>(UUID.randomUUID());
TreeItem<UUID> node2 = new TreeItem<>(UUID.randomUUID());
IntStream.range(0, 100)
.mapToObj(index -> new TreeItem<>(UUID.randomUUID()))
.forEach(node2.getChildren()::add);
root.getChildren().setAll(node0, node1, node2);
root.setExpanded(true);
node2.setExpanded(true);
BorderPane pane = new BorderPane();
pane.setCenter(tree);
Scene scene = new Scene(pane, 600, 600);
primaryStage.setTitle("Demo");
primaryStage.setScene(scene);
primaryStage.setOnCloseRequest(t -> Platform.exit());
primaryStage.show();
}
static class CustomCell extends TreeCell<UUID> {
public HBox hBox;
public Label lbLeft;
public Label lbRight;
public CustomCell() {
hBox = new HBox();
lbLeft = new Label();
lbRight = new Label();
lbRight.setStyle("-fx-padding: 0 20 0 0");
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
hBox.getChildren().setAll(lbLeft, spacer, lbRight);
}
#Override
protected void updateItem(UUID uuid, boolean empty) {
super.updateItem(uuid, empty);
if (empty) {
setGraphic(null);
return;
}
String s = uuid.toString();
lbLeft.setText(s.substring(0, 6));
lbRight.setText(s.substring(6, 12));
setGraphic(hBox);
}
}
}
Reacting to
you can't just extend the VirtualFlow and override a method
certainly true if the method is deeply hidden by package/-private access (but even then: javafx is open source, checkout-edit-compile-distribute is also an option :). In this case we might get along with overriding public api as outlined below (not formally tested!).
VirtualFlow is the "layout" of cells and scrollBars: in particular, it has to cope with handling sizing/locating of all content w/out scrollBars being visible. There are options on how that can be done:
adjust cell width to always fill the viewport, increasing/decreasing when vertical scrollBar is hidden/visible
keep cell width constant such that there is always space left for the scrollBar, be it visible or not
keep cell width constant such that there is never space left the scrollBar, laying it out on top of cell
others ??
Default VirtualFlow implements the first with no option to switch to any other. (might be candidate for an RFE, feel free to report :).
Digging into the code reveals that the final sizing of the cells is done by calling cell.resize(..) (as already noted and exploited in the self-answer) near the end of the layout code. Overriding a custom cell's resize is perfectly valid and a good option .. but not the only one, IMO. An alternative is to
extend VirtualFlow and override layoutChildren to adjust cell width as needed
extend TreeViewSkin to use the custom flow
Example code (requires fx12++):
public static class XVirtualFlow<I extends IndexedCell> extends VirtualFlow<I> {
#Override
protected void layoutChildren() {
super.layoutChildren();
fitCellWidths();
}
/**
* Resizes cell width to accomodate for invisible vbar.
*/
private void fitCellWidths() {
if (!isVertical() || getVbar().isVisible()) return;
double width = getWidth() - getVbar().getWidth();
for (I cell : getCells()) {
cell.resize(width, cell.getHeight());
}
}
}
public static class XTreeViewSkin<T> extends TreeViewSkin<T>{
public XTreeViewSkin(TreeView<T> control) {
super(control);
}
#Override
protected VirtualFlow<TreeCell<T>> createVirtualFlow() {
return new XVirtualFlow<>();
}
}
On-the-fly usage:
TreeView<UUID> tree = new TreeView<>(root) {
#Override
protected Skin<?> createDefaultSkin() {
return new XTreeViewSkin<>(this);
}
};
Ok, this is summary based on #kleopatra comments and OpenJFX code exploration. There will be no code to solve the problem, but still maybe it will spare some time to someone.
As being said, it's VirtualFlow responsibility to manage virtualized control viewport size. All magic happens in the layoutChildren(). First it computes scrollbars visibility and then recalculates size of all children based on that knowledge. Here is the code which causes the problem.
Since all implementation details are private or package-private, you can't just extend the VirtualFlow and override method or two, you have to copy-paste and edit entire class (to remove one line, yes). Given that, changing internal components layout could be a better option.
Sometimes, I adore languages those have no encapsulation.
UPDATE:
I've solved the problem. There is no way no reserve space for vertical scrollbar without tweaking JavaFX internals, but we can limit cell width, so it would be always less than TreeView (or List View) width. Here is simple example.
public class Launcher extends Application {
public static final double SCENE_WIDTH = 500;
#Override
public void start(Stage primaryStage) {
TreeItem<UUID> root = new TreeItem<>(UUID.randomUUID());
TreeView<UUID> tree = new TreeView<>(root);
tree.setCellFactory(list -> new CustomCell(SCENE_WIDTH));
TreeItem<UUID> node0 = new TreeItem<>(UUID.randomUUID());
TreeItem<UUID> node1 = new TreeItem<>(UUID.randomUUID());
TreeItem<UUID> node2 = new TreeItem<>(UUID.randomUUID());
IntStream.range(0, 100)
.mapToObj(index -> new TreeItem<>(UUID.randomUUID()))
.forEach(node2.getChildren()::add);
root.getChildren().setAll(node0, node1, node2);
root.setExpanded(true);
node2.setExpanded(true);
BorderPane pane = new BorderPane();
pane.setCenter(tree);
Scene scene = new Scene(pane, SCENE_WIDTH, 600);
primaryStage.setTitle("Demo");
primaryStage.setScene(scene);
primaryStage.setOnCloseRequest(t -> Platform.exit());
primaryStage.show();
}
static class CustomCell extends TreeCell<UUID> {
public static final double RIGHT_PADDING = 40;
/*
this value depends on tree disclosure node width
in my case it's enforced via CSS, so I always know exact
value of this padding
*/
public static final double INDENT_PADDING = 14;
public HBox hBox;
public Label lbLeft;
public Label lbRight;
public double maxWidth;
public CustomCell(double maxWidth) {
this.maxWidth = maxWidth;
hBox = new HBox();
lbLeft = new Label();
lbRight = new Label();
lbRight.setPadding(new Insets(0, RIGHT_PADDING, 0, 0));
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
hBox.getChildren().setAll(lbLeft, spacer, lbRight);
}
#Override
protected void updateItem(UUID uuid, boolean empty) {
super.updateItem(uuid, empty);
if (empty) {
setGraphic(null);
return;
}
String s = uuid.toString();
lbLeft.setText(s.substring(0, 6));
lbRight.setText(s.substring(6, 12));
setGraphic(hBox);
}
#Override
public void resize(double width, double height) {
// enforce item width
double maxCellWidth = getTreeView().getWidth() - RIGHT_PADDING;
double startLevel = getTreeView().isShowRoot() ? 0 : 1;
double itemLevel = getTreeView().getTreeItemLevel(getTreeItem());
if (itemLevel > startLevel) {
maxCellWidth = maxCellWidth - ((itemLevel - startLevel) * INDENT_PADDING);
}
hBox.setPrefWidth(maxCellWidth);
hBox.setMaxWidth(maxCellWidth);
super.resize(width, height);
}
}
}
It's far from perfect, but it works.
I am trying to implement a full press-drag-release gesture with JavaFX. I want to drag a rectangle from one VBox to another. On the MOUSE_DRAG_RELEASED event that happens on the target VBox, I'm trying to add the dragged rectangle as a child of the target VBox.
The problem is that when I release the mouse on the target VBox, the rectangle does not get into the expected position inside the VBox, but is always offset to the right by a fixed distance.
public class DragFromOneVBoxToAnother extends Application {
private Disk sourceDisk = new Disk();
private VBox targetVBox = new VBox();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) {
// Build the UI
GridPane root = getUI();
// Add the event handlers
this.addEventHandlers();
Scene scene = new Scene(root, 800, 600);
stage.setScene(scene);
stage.show();
}
private GridPane getUI() {
GridPane pane = new GridPane();
VBox sourceVBox = new VBox();
sourceDisk.setWidth(90);
sourceDisk.setHeight(20);
sourceVBox.setStyle(" -fx-border-color:red; -fx-border-width: 1; -fx-border-style: solid;");
targetVBox.setStyle(" -fx-border-color:green; -fx-border-width: 1; -fx-border-style: solid;");
sourceVBox.getChildren().add(sourceDisk);
targetVBox.getChildren().add(new Rectangle(200, 20));
pane.setHgap(200);
pane.addColumn(0, sourceVBox);
pane.addColumn(1, targetVBox);
pane.setPadding(new Insets(200, 100, 200, 100));
return pane;
}
private void addEventHandlers() {
sourceDisk.setOnMouseEntered(event -> sourceDisk.setCursor(Cursor.HAND));
sourceDisk.setOnMousePressed(event -> {
sourceDisk.setOrgSceneX(event.getSceneX());
sourceDisk.setOrgSceneY(event.getSceneY());
sourceDisk.setOrgTranslateX(sourceDisk.getTranslateX());
sourceDisk.setOrgTranslateY(sourceDisk.getTranslateY());
sourceDisk.setMouseTransparent(true);
sourceDisk.setCursor(Cursor.CLOSED_HAND);
});
sourceDisk.setOnDragDetected(event -> sourceDisk.startFullDrag());
sourceDisk.setOnMouseDragged(event -> {
double offsetX = event.getSceneX() - sourceDisk.getOrgSceneX();
double offsetY = event.getSceneY() - sourceDisk.getOrgSceneY();
double newTranslateX = sourceDisk.getOrgTranslateX() + offsetX;
double newTranslateY = sourceDisk.getOrgTranslateY() + offsetY;
sourceDisk.setTranslateX(newTranslateX);
sourceDisk.setTranslateY(newTranslateY);
});
sourceDisk.setOnMouseReleased(event -> {
sourceDisk.setMouseTransparent(false);
sourceDisk.setCursor(Cursor.DEFAULT);
});
targetVBox.setOnMouseDragReleased(event ->
targetVBox.getChildren().add(sourceDisk));
}
private class Disk extends Rectangle {
private double orgSceneX;
private double orgSceneY;
private double orgTranslateX;
private double orgTranslateY;
// below, the getters and setters for all the instance variables
// were removed for brevity
}
I have found that, even though the visual representation of the dragged rectangle seems to be offset when it's dropped, a child appears to actually be added to the target VBox (this can be seen because the border of the VBox expands after the MOUSE_DRAG_RELEASED event).
What could be the issue?
During the mouse drag gesture you modify the translateX/translateY properties of the node. This results in the dragged node being offset from the position where the new parent places it by this transformation. You need to reset those values to properly add the node to the bottom of the VBox:
targetVBox.setOnMouseDragReleased(event -> {
targetVBox.getChildren().add(sourceDisk);
// reset translate values
sourceDisk.setTranslateX(0);
sourceDisk.setTranslateY(0);
});
I want a stack pane to simulate a FullHD display, that is 1920x1080.
For that I am using 640x360 which follows the same proportion and then I scale the image nodes inside the stackPane (which is called "screen") using:
image.getWidth()/(fullWidth/screen.getWidth())),
image.getHeight()/(fullHeight/screen.getHeight()))
This is working great only problem is I can't set the size for the "screen" therefore it keeps big black bars on the bottom and on the top of it.
As you can see in the image below the "screen" has a white border around it for making the black bars easier to notice.
screen snapshot
Here is the code that creates the stack pane and handles it:
DisplayPane constructor method
public DisplayPane(TemporalViewPane temporalViewPane, SteveMenuBar steveMenuBar, SpatialTemporalView spatialTemporalView){
setId("display-pane");
screen = new StackPane();
screen.setId("screen-pane");
controlButtonPane = new ControlButtonPane(screen, temporalViewPane, steveMenuBar, spatialTemporalView);
setCenter(screen);
setBottom(controlButtonPane);
}
ControlButtonPane constructor class method:
public ControlButtonPane(StackPane screen, TemporalViewPane temporalViewPane,SteveMenuBar steveMenuBar, SpatialTemporalView spatialTemporalView){
fullHDLabel = new Label("FullHD");
fullHDLabel.setPadding(new Insets(5,5,5,5));
screen.getChildren().add(fullHDLabel);
setId("control-button-pane");
this.steveMenuBar = steveMenuBar;
this.screen = screen;
this.screen.setAlignment(Pos.TOP_LEFT);
this.temporalViewPane = temporalViewPane;
this.spatialTemporalView = spatialTemporalView;
this.webView = new WebView();
createButtons();
setLeft(fullButtonPane);
setLeft(fullHDLabel);
setCenter(centerButtonPane);
setRight(refreshButtonPane);
createListeners();
createButtonActions();
}
CreateButtons method:
public void createButtons(){
run = new Button();
run.setDisable(true);
run.setId("run-button");
run.setTooltip(new Tooltip(Language.translate("run")));
play = new Button();
play.setDisable(true);
play.setId("play-button");
play.setTooltip(new Tooltip(Language.translate("play")));
pause = new Button();
pause.setDisable(true);
pause.setId("pause-button");
pause.setTooltip(new Tooltip(Language.translate("pause")));
stop = new Button();
stop.setDisable(true);
stop.setId("stop-button");
stop.setTooltip(new Tooltip(Language.translate("stop")));
centerButtonPane = new HBox();
centerButtonPane.setId("center-button-pane");
centerButtonPane.getChildren().add(run);
centerButtonPane.getChildren().add(play);
centerButtonPane.getChildren().add(pause);
centerButtonPane.getChildren().add(stop);
}
CreateListeners method:
private void createListeners(){
screen.widthProperty().addListener((obs, oldVal, newVal) -> {
if(newVal != oldVal){
screen.minHeightProperty().bind(screen.widthProperty().multiply(0.565));
screen.maxHeightProperty().bind(screen.widthProperty().multiply(0.565));
System.out.println("NEW WIDTH OF SCREEN IS: "+newVal);
}
});
screen.heightProperty().addListener((obs, oldVal, newVal) -> {
if(newVal != oldVal){
screen.minHeightProperty().bind(screen.widthProperty().multiply(0.565));
screen.maxHeightProperty().bind(screen.widthProperty().multiply(0.565));
System.out.println("NEW HEIGHT OF SCREEN IS: "+newVal);
}
});
And below is what I want to achieve. Currently, I have to run the application and then drag the side of the stack pane for resizing it as it should be.
how it should be snapshot
I've tried all set min/max/pref height/width and still haven't manage to achieve my goal. I don't know what else to do.
Any help would be great.
Problem was I was binding height to width when it should be the other way around since width can be resized much larger without wasting screen space.
So I changed the binding to:
private void createListeners(){
screen.widthProperty().addListener((obs, oldVal, newVal) -> {
if(newVal != oldVal){
screen.minWidthProperty().bind(screen.heightProperty().multiply(1.77778));
screen.maxWidthProperty().bind(screen.heightProperty().multiply(1.77778));
System.out.println("NEW WIDTH OF SCREEN IS: "+newVal);
}
});
screen.heightProperty().addListener((obs, oldVal, newVal) -> {
if(newVal != oldVal){
screen.minWidthProperty().bind(screen.heightProperty().multiply(1.77778));
screen.maxWidthProperty().bind(screen.heightProperty().multiply(1.77778));
System.out.println("NEW HEIGHT OF SCREEN IS: "+newVal);
}
});
}
How I can resize Pane in GridPane? I'm creating a GUI which is GridPane and a map in it. And when gui change it's size I want to map which is in another Pane to resize. So I've added listners to width and height property of scene and there are generaly working. When I do setTranslateX of map I can see that something changed, but resizing not works at all. It can be caused by fact that map contains from tiles which have their size set. Code of method which create the scene:
private Scene createScene(GridPane root){
Scene scene = new Scene(root, WIDTH_OF_SCENE, HEIGHT_OF_SCENE);
scene.widthProperty().addListener((observableValue, oldSceneWidth, newSceneWidth) -> {
WIDTH_OF_SCENE = newSceneWidth.intValue();
draw();
MAP_PANE.setPrefSize(200, 200); //it's not working
MAP_PANE.setTranslateX(0); //it's working
root.requestLayout();
});
scene.heightProperty().addListener((observableValue, oldSceneHeight, newSceneHeight) ->{
HEIGHT_OF_SCENE = newSceneHeight.intValue();
draw();
root.requestLayout();
});
return scene;
}
And here I'm creating MAP_PANE:
public Pane createContent() {
map = new Pane();
map.setStyle("-fx-background-color: #383838");
// dodawanie pojedynczych kafli do dwuwymiarowej tablicy
for (int y = 0; y < Y_TILES; y++) {
for (int x = 0; x < X_TILES; x++) {
Tile tile = new Tile(x, y);
grid[x][y] = tile;
map.getChildren().addAll(tile);
}
}
return map;
}
And start method:
#Override
public void start(Stage primaryStage) throws Exception{
Map map = new Map();
GridPane root = FXMLLoader.load(getClass().getResource("sample.fxml"));
MAP_PANE = map.createContent();
IMAGE_VIEW = new ImageView();
countScreenSize();
draw();
root.getChildren().addAll(IMAGE_VIEW);
root.getChildren().addAll(MAP_PANE);
primaryStage.setTitle(TITLE);
primaryStage.setScene(createScene(root));
primaryStage.show();
}
I'm trying to avoid horizontal scrolling in ListView. The ListView instance holds list of HBox items, each item has a different width.
So far I'm using such a cell factory:
public class ListViewCell extends ListCell<Data>
{
#Override
public void updateItem(Data data, boolean empty)
{
super.updateItem(data, empty);
if(empty || data == null){
setGraphic(null);
setText(null);
}
if(data != null)
{
Region region = createRow(data);
region.prefWidthProperty().bind(mListView.widthProperty().subtract(20));
region.maxWidthProperty().bind(mListView.widthProperty().subtract(20));
setGraphic(region);
}
}
}
Unfortunately it is not enough. Usually after adding several items ListView's horizontal scrollbar appears. Even if it seems to be unnecessary.
How can I assure, that ListViewCell will not exceed it's parent width and horizontal scrollbar will not appear?
There is a lot at play here that make customizing ListView horizontal scrollbar behavior difficult to deal with. In addition to that, common misunderstandings on how ListView works can cause other problems.
The main issue to address is that the width of the ListCells will not automatically adapt when the vertical scrollbar becomes visible. Therefore, the moment it is, suddenly the contents are too wide to fit between the left edge of the ListView and the left edge of the vertical scrollbar, triggering a horizontal scrollbar. There is also the default padding of a ListCell as well as the border widths of the ListView itself to consider when determining the proper binding to set.
The following class that extends ListView:
public class WidthBoundList extends ListView {
private final BooleanProperty vbarVisibleProperty = new SimpleBooleanProperty(false);
private final boolean bindPrefWidth;
private final double scrollbarThickness;
private final double sumBorderSides;
public WidthBoundList(double scrollbarThickness, double sumBorderSides, boolean bindPrefWidth) {
this.scrollbarThickness = scrollbarThickness;
this.sumBorderSides = sumBorderSides;
this.bindPrefWidth = bindPrefWidth;
Platform.runLater(()->{
findScroller();
});
}
private void findScroller() {
if (!this.getChildren().isEmpty()) {
VirtualFlow flow = (VirtualFlow)this.getChildren().get(0);
if (flow != null) {
List<Node> flowChildren = flow.getChildrenUnmodifiable();
int len = flowChildren .size();
for (int i = 0; i < len; i++) {
Node n = flowChildren .get(i);
if (n.getClass().equals(VirtualScrollBar.class)) {
final ScrollBar bar = (ScrollBar) n;
if (bar.getOrientation().equals(Orientation.VERTICAL)) {
vbarVisibleProperty.bind(bar.visibleProperty());
bar.setPrefWidth(scrollbarThickness);
bar.setMinWidth(scrollbarThickness);
bar.setMaxWidth(scrollbarThickness);
} else if (bar.getOrientation().equals(Orientation.HORIZONTAL)) {
bar.setPrefHeight(scrollbarThickness);
bar.setMinHeight(scrollbarThickness);
bar.setMaxHeight(scrollbarThickness);
}
}
}
} else {
Platform.runLater(()->{
findScroller();
});
}
} else {
Platform.runLater(()->{
findScroller();
});
}
}
public void bindWidthScrollCondition(Region node) {
node.maxWidthProperty().unbind();
node.prefWidthProperty().unbind();
node.maxWidthProperty().bind(
Bindings.when(vbarVisibleProperty)
.then(this.widthProperty().subtract(scrollbarThickness).subtract(sumBorderSides))
.otherwise(this.widthProperty().subtract(sumBorderSides))
);
if (bindPrefWidth) {
node.prefWidthProperty().bind(node.maxWidthProperty());
}
}
}
Regarding your code, your bindings could cause problems. A ListCell's updateItem() method is not only called when the ListCell is created. A ListView can contain a pretty large list of data, so to improve the performance only the ListCells scrolled into view (and possibly a few before and after) need their graphic rendered. The updateItem() method handles this. In your code, a Region is being created over and over again and each and every one of them is being bound to the width of your ListView. Instead, the ListCell itself should be bound.
The following class extends ListCell and the method to bind the HBox is called in the constructor:
public class BoundListCell extends ListCell<String> {
private final HBox hbox;
private final Label label;
public BoundListCell(WidthBoundList widthBoundList) {
this.setPadding(Insets.EMPTY);
hbox = new HBox();
label = new Label();
hbox.setPadding(new Insets(2, 4, 2, 4));
hbox.getChildren().add(label);
widthBoundList.bindWidthScrollCondition(this);
}
#Override
public void updateItem(String data, boolean empty) {
super.updateItem(data, empty);
if (empty || data == null) {
label.setText("");
setGraphic(null);
setText(null);
} else {
label.setText(data);
setGraphic(hbox);
}
}
}
The scrollbarThickness parameter of WidthBoundList constructor has been set to 12. The sumBorderSides parameter has been set to 2 because my WidthBoundList has a one pixel border on the right and left. The bindPrefWidth parameter has been set to true to prevent the horizontal scroller from showing at all (labels have ellipses, any non-text nodes that you might add to the hbox will simply be clipped). Set bindPrefWidth to false to allow a horizontal scrollbar, and with these proper bindings it should only show when needed. An implementation:
private final WidthBoundList myListView = new WidthBoundList(12, 2, true);
public static void main(final String... a) {
Application.launch(a);
}
#Override
public void start(final Stage primaryStage) throws Exception {
myListView.setCellFactory(c -> new BoundListCell(myListView));
VBox vBox = new VBox();
vBox.setFillWidth(true);
vBox.setAlignment(Pos.CENTER);
vBox.setSpacing(5);
Button button = new Button("APPEND");
button.setOnAction((e)->{
myListView.getItems().add("THIS IS LIST ITEM NUMBER " + myListView.getItems().size());
});
vBox.getChildren().addAll(myListView, button);
myListView.maxWidthProperty().bind(vBox.widthProperty().subtract(20));
myListView.prefHeightProperty().bind(vBox.heightProperty().subtract(20));
primaryStage.setScene(new Scene(vBox, 200, 400));
primaryStage.show();
}