JavaFX: scrolling vs. focus traversal with arrow keys


Question

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.

1
6
10/31/2013 11:36:00 AM

Accepted Answer

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);
  }
}
4
10/28/2013 6:49:04 AM

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon