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.
Related
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 have a TableView that scrolls vertically, and I would like the ScrollBar to extend to the top of it's parent AnchorPane and to be on top of the filler square at the top right. See below for what it's like by default. Note that my filler node is white, that is not a table column at the top right.
and below this line is what I want, correctly implemented by another program.
I was able to achieve this by doing
Platform.runLater(() ->
{
ScrollBar someScrollBar = (ScrollBar) someTable.lookup(".scroll-bar:vertical");
someScrollBar.setTranslateY(-12);
someScrollBar.setScaleY(1.2);
}
);
where someTable is a TableView made in FXML and is referred to in the controller initialize function.
It looks fine like this, but it doesn't scale correctly. If the containing AnchorPane resizes vertically, it looks awful.
Can anyone suggest a better way to do this?
Thank you so much for your time.
Custom layout of the scrollBars is not supported. And my initial comment you need a custom TableViewSkin with a custom TableHeaderRow: the latter is responsible for managing the .. well, tableHeader is only part of the story, unfortunately.
a TableHeaderRow indeed is responsible for laying out the table header
but: a TableHeaderRow can do nothing to layout the vertical scrollbar - when it tries to do so in an overridden layoutChildren() it's immediately reset
the reset happens in the VirtualFlow (which is the parent of the scrollBar)
So at the end of the day, we need
a custom TableHeaderRow that signals the need for enlarging and relocating the scrollBar: the example below sets a marker if the scrollBar is visible (tbd: check whether or not the menuButton is visible) with the desired additional height in the scrollBar's properties map
a custom VirtualFlow that can handle the marker and actually does the layout as needed: the example below checks for the marker and resizes/relocates the scrollBar if needed.
a custom TableViewSkin to inject both (via overridden factory methods)
The example is written against fx11, should work for fx10 but not for fx9 because the latter doesn't allow to provide a custom VirtualFlow:
public class TableWithoutCorner extends Application {
/**
* Custom TableHeaderRow that requests a larger vbar height
* if needed.
*/
private static class MyTableHeader extends TableHeaderRow {
private Region cornerAlias;
private ScrollBar vBar;
private TableViewSkinBase skin;
public MyTableHeader(TableViewSkinBase skin) {
super(skin);
this.skin = skin;
}
#Override
protected void layoutChildren() {
super.layoutChildren();
adjustCornerLayout();
}
private void adjustCornerLayout() {
checkAlias();
// tbd: check also if corner is visible
if (!vBar.isVisible()) {
vBar.getProperties().remove("DELTA");
} else {
vBar.getProperties().put("DELTA", getHeight());
}
}
private void checkAlias() {
if (cornerAlias == null) {
cornerAlias = (Region) lookup(".show-hide-columns-button");
}
if (vBar == null) {
vBar = (ScrollBar) skin.getSkinnable().lookup(".scroll-bar:vertical");
}
}
}
/**
* Custom VirtualFlow that respects additinal height for its
* vertical ScrollBar.
*/
private static class MyFlow extends VirtualFlow {
private ScrollBar vBar;
private Region clip;
public MyFlow() {
// the scrollbar to adjust
vBar = (ScrollBar) lookup(".scroll-bar:vertical");
// the clipped container to use for accessing viewport dimensions
clip = (Region) lookup(".clipped-container");
}
/**
* Overridden to adjust vertical scrollbar's height and y-location
* after calling super.
*/
#Override
protected void layoutChildren() {
super.layoutChildren();
adjustVBar();
}
/**
* Adjusts vBar height and y-location by the height as
* requested by the table header.
*/
protected void adjustVBar() {
if (vBar.getProperties().get("DELTA") == null) return;
double delta = (double) vBar.getProperties().get("DELTA");
vBar.relocate(clip.getWidth(), - delta);
vBar.resize(vBar.getWidth(), clip.getHeight() + delta);
}
}
/**
* Boilerplate: need custom TableViewSkin to inject a custom TableHeaderRow and
* custom VirtualFlow.
*/
private static class MyTableViewSkin<T> extends TableViewSkin<T> {
public MyTableViewSkin(TableView<T> control) {
super(control);
}
#Override
protected TableHeaderRow createTableHeaderRow() {
return new MyTableHeader(this);
}
#Override
protected VirtualFlow<TableRow<T>> createVirtualFlow() {
return new MyFlow();
}
}
private Parent createContent() {
TableView<Locale> table = new TableView<>(FXCollections.observableArrayList(Locale.getAvailableLocales())) {
#Override
protected Skin<?> createDefaultSkin() {
return new MyTableViewSkin(this);
}
};
TableColumn<Locale, String> col = new TableColumn<>("Name");
col.setCellValueFactory(new PropertyValueFactory<>("displayName"));
table.getColumns().addAll(col);
return table;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
//stage.setTitle(FXUtils.version());
stage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TableWithoutCorner.class.getName());
}
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);
}
});
}
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();
}
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.