3

I have an ScrollPane containing a TilePane which shows images. Whenever I delete one of those images the ScrollPane jumps back to the top which is quite annoying when trying to remove multiple images. Is there a way to control the scrolling behaviour?

I'm running this code on Windows 7:

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.TilePane;
import javafx.stage.Stage;

public class ScrollbarProblem extends Application{
    private TilePane imagePane;
    private ScrollPane scroller;

    @Override
    public void start(Stage primaryStage) {
        imagePane = new TilePane();
        scroller = new ScrollPane();
        scroller.setContent(imagePane);
        BorderPane root = new BorderPane(scroller, null, null, null, null);
        Scene scene = new Scene(root, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();

        for(int i = 0; i < 20; i++){
            Image img = new Image("https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/SIPI_Jelly_Beans_4.1.07.tiff/lossy-page1-220px-SIPI_Jelly_Beans_4.1.07.tiff.jpg");
            final ImageView imageView = new ImageView(img);

            final Button button = new Button("Delete");
            final StackPane stackPane = new StackPane();

            button.setOnAction(new EventHandler<ActionEvent>() {

                @Override
                public void handle(ActionEvent event) {
                    imagePane.getChildren().remove(stackPane);
                }
            });

            stackPane.getChildren().addAll(imageView, button);
            imagePane.getChildren().add(stackPane);
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
fabian
  • 80,457
  • 12
  • 86
  • 114
Simoon
  • 33
  • 4

3 Answers3

5

ScrollPane makes sure the focused Node is shown inside the viewport. A Button that is clicked has the focus. This means the focused node is removed and JavaFX tries to move the focus to another node in the ScrollPane content, which in this case is the first Button. ScrollPane scrolls this node into view, which explains the observed behavior.

A solution would be setting the focusTraversable property of every Button to false.

Another option which would still allow you to change the focus via Tab would be overriding the onTraverse method of the ScrollPaneSkin, since this is the method responsible for this behavior:

scroller.setSkin(new ScrollPaneSkin(scroller) {

    @Override
    public void onTraverse(Node n, Bounds b) {
    }

});
fabian
  • 80,457
  • 12
  • 86
  • 114
3

The problem seems to be caused by the focus reverting to the button in the first element of the tile pane. One workaround is:

Image img = new Image(
        "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/SIPI_Jelly_Beans_4.1.07.tiff/lossy-page1-220px-SIPI_Jelly_Beans_4.1.07.tiff.jpg");

for (int i = 0; i < 20; i++) {
    final ImageView imageView = new ImageView(img);

    final Button button = new Button("Delete");
    final StackPane stackPane = new StackPane();


    button.setOnAction(new EventHandler<ActionEvent>() {

        @Override
        public void handle(ActionEvent event) {

            int index = imagePane.getChildren().indexOf(stackPane);

            imagePane.getChildren().remove(index);
            int numPanes = imagePane.getChildren().size();
            if (numPanes > 0) {
                imagePane.getChildren().get(Math.min(index, numPanes-1)).requestFocus();
            }
        }
    });

    stackPane.getChildren().addAll(imageView, button);
    imagePane.getChildren().add(stackPane);
}
James_D
  • 201,275
  • 16
  • 291
  • 322
0

this answer is base on Fabian answer. in some JavaFx imp there is no onTraverse method on ScrollPaneSkin(see javafx.scene.control.skin.ScrollPaneSkin vs javafx.scene.control.skin.ScrollPaneSkin) so it can't be @Override, so I work for a general solution that will not include the need for every button change by the programmer, but it can get the root and set the FocusTravers= false whatever Node(Button in this imp) is added to the view tree.

This helper class:

public class FocusTraversHelper {

    static private ListChangeListener listChangeListener;
    static {
        listChangeListener=new ListChangeListener<Node>() {
            @Override
            public void onChanged(Change<? extends Node> c) {
                if(c.next()&&c.wasAdded()){
                    Node b =c.getAddedSubList().get(0);
                    if(b!=null&&(b instanceof Parent || b instanceof Button) ){
                        cancelFocusTravers(b);
                    }
                }

            }};
    }
    static private void registerAddEvent(Parent parent){
        parent.getChildrenUnmodifiable().removeListener(listChangeListener);
        parent.getChildrenUnmodifiable().addListener(listChangeListener);
    }
    static public void cancelFocusTravers(Node node){
        if(node instanceof Parent){
            registerAddEvent((Parent)node);
            for (Node n: ((Parent) node).getChildrenUnmodifiable()) {
                cancelFocusTravers(n);
            }
        }
        if(node instanceof Button){
            node.setFocusTraversable(false);
        }
    }
}

and the use is like:

  FocusTraversHelper.cancelFocusTravers(root);  

Hope it will help someone

Note: if there is another control like TextField it should be modified in order to work(maybe use Node instead of Button).

Note: the remove->add needed in case of remove and add the same node multiple times, so there will be only one listener

Sarel Foyerlicht
  • 907
  • 6
  • 11