Search code examples
focusjavafxscrollviewkey-bindingsjavafx-8

JavaFX: scrolling vs. focus traversal with arrow keys


I got a ScrollPane containing focusable Nodes.

The current default behaviour is:

  • Shift + , , , moves the focus

  • , , , scrolls the view

I want it the other way around. How can I accomplish this or where should I start?


[EDIT] Well, there is another fragile approach.

Instead of messing around with the events, one could mess around with the KeyBindings.

    scrollPane.skinProperty().addListener(new ChangeListener<Skin<?>>() {
        @Override
        public void changed(ObservableValue<? extends Skin<?>> observable, Skin<?> oldValue, Skin<?> newValue) {
            ScrollPaneSkin scrollPaneSkin = (ScrollPaneSkin) scrollPane.getSkin();
            ScrollPaneBehavior scrollPaneBehavior = scrollPaneSkin.getBehavior();
            try {
                Field keyBindingsField = BehaviorBase.class.getDeclaredField("keyBindings");
                keyBindingsField.setAccessible(true);
                List<KeyBinding> keyBindings = (List<KeyBinding>) keyBindingsField.get(scrollPaneBehavior);
                List<KeyBinding> newKeyBindings = new ArrayList<>();
                for (KeyBinding keyBinding : keyBindings) {
                    KeyCode code = keyBinding.getCode();
                    newKeyBindings.add(code == KeyCode.LEFT || code == KeyCode.RIGHT || code == KeyCode.UP || code == KeyCode.DOWN ? keyBinding.shift() : keyBinding);
                }
                keyBindingsField.set(scrollPaneBehavior, newKeyBindings);
            } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
                LOGGER.warn("private api changed.", e);
            }
        }
    });

I think, that could be the cleaner way, if KeyBindings were more non-static, modifyable and public.


Solution

  • Use an event filter to capture the relevant key events and remap them to different key events before the events start to bubble.

    Re-mapping default keys is a tricky thing which:

    1. Can confuse the user.
    2. May have unexpected side effects (e.g. TextFields may no longer work as you expect).

    So use with care:

    import javafx.application.*;
    import javafx.event.*;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.input.KeyEvent;
    import javafx.scene.layout.TilePane;
    import javafx.stage.Stage;
    
    import java.util.*;
    
    public class ScrollInterceptor extends Application {
    
      @Override
      public void start(Stage stage) {
        ScrollPane scrollPane = new ScrollPane(
          createScrollableContent()
        );
    
        Scene scene = new Scene(
          scrollPane,
          300, 200
        );
    
        remapArrowKeys(scrollPane);
    
        stage.setScene(scene);
        stage.show();
    
        hackToScrollToTopLeftCorner(scrollPane);
      }
    
      private void remapArrowKeys(ScrollPane scrollPane) {
        List<KeyEvent> mappedEvents = new ArrayList<>();
        scrollPane.addEventFilter(KeyEvent.ANY, new EventHandler<KeyEvent>() {
          @Override
          public void handle(KeyEvent event) {
            if (mappedEvents.remove(event))
              return;
    
            switch (event.getCode()) {
              case UP:
              case DOWN:
              case LEFT:
              case RIGHT:
                KeyEvent newEvent = remap(event);
                mappedEvents.add(newEvent);
                event.consume();
                Event.fireEvent(event.getTarget(), newEvent);
            }
          }
    
          private KeyEvent remap(KeyEvent event) {
            KeyEvent newEvent = new KeyEvent(
                event.getEventType(),
                event.getCharacter(),
                event.getText(),
                event.getCode(),
                !event.isShiftDown(),
                event.isControlDown(),
                event.isAltDown(),
                event.isMetaDown()
            );
    
            return newEvent.copyFor(event.getSource(), event.getTarget());
          }
        });
      }
    
      private TilePane createScrollableContent() {
        TilePane tiles = new TilePane();
        tiles.setPrefColumns(10);
        tiles.setHgap(5);
        tiles.setVgap(5);
        for (int i = 0; i < 100; i++) {
          Button button = new Button(i + "");
          button.setMaxWidth(Double.MAX_VALUE);
          button.setMaxHeight(Double.MAX_VALUE);
          tiles.getChildren().add(button);
        }
        return tiles;
      }
    
      private void hackToScrollToTopLeftCorner(final ScrollPane scrollPane) {
        Platform.runLater(new Runnable() {
          @Override
          public void run() {
            scrollPane.setHvalue(scrollPane.getHmin());
            scrollPane.setVvalue(0);
          }
        });
      }
    
      public static void main(String[] args) {
        launch(args);
      }
    }