JavaFX фокусируется на содержимом вкладки при переключении - PullRequest
1 голос
/ 26 февраля 2020

У меня есть TabPane с TextArea внутри каждого из его Tabs.
Чего я хочу добиться, так это при переключении вкладок, textArea фокусируется.
Я пытался со слушателем, но это не похоже на работу:

@FXML
public void initialize() {
  for(Tab tab : tabPane.getTabs())
  {
    tab.setOnSelectionChanged(event->
    {
        if(tab.isSelected())
        {
            System.out.println(tab.getText());
            TextArea ta = (TextArea)((AnchorPane)tab.getContent()).getChildren().get(0);
            ta.requestFocus();
        }
    });
  }
}

Когда я переключаю вкладки, вывод показывает заголовок активной вкладки, но он остается сфокусированным, как я могу сфокусироваться на TextArea после переключения?

Спасибо!

1 Ответ

2 голосов
/ 28 февраля 2020

Хотя не удивительно, что node.requestFocus() не фокусирует узел, как ожидалось (с обычным слегка вонючим способом обернуть его в Platform.runlater()), мне интересно, почему точно это не ' t работать в этом контексте.

Оказалось, что одной технической причиной является то, что на момент получения уведомления любым из свойств выбора (selectedItem / -Index, isSelected) узел еще не находился в видимой родительской иерархии. - так что это не может быть действительной целью фокуса. Чтобы увидеть, добавьте println в обработчик onSelected:

Node tabContent = tab.getContent();
if (tab.isSelected() && tab.getContent() != null && tab.getContent().getParent() != null ) {
    System.out.println("onSelection " + tab.getText() 
    + tabContent.getParent().isVisible());
}

Это связано с разметкой обложки / управлением вкладками: содержимое каждой из них упаковано в специализированную StackPane (TabContentRegion), все они наложены на друг над другом только с выбранным с его свойством видимости true.

Таким образом, первое приближение для решения состоит в том, чтобы зарегистрировать слушателя для свойства видимости этого контейнера: при изменении на true его дочерние элементы должны быть пригодны в качестве целей фокуса. Что на самом деле они ... просто ... TabPaneBehavior вмешивается, заставляя фокус на самой tabPane всякий раз, когда выбор изменяется в результате взаимодействия с пользователем (как путем нажатия на заголовок вкладки, так и с помощью Ctrl-Tab)

// unconditionally by mouse
new MouseMapping(MouseEvent.MOUSE_PRESSED, e -> getNode().requestFocus())

// method called by keyMappings that move the selection
private void moveSelection(int startIndex, int delta) {
    final TabPane tabPane = getNode();
    if (tabPane.getTabs().isEmpty()) return;

    int tabIndex = findValidTab(startIndex, delta);
    if (tabIndex > -1) {
        final SelectionModel<Tab> selectionModel = tabPane.getSelectionModel();
        selectionModel.select(tabIndex);
    }
    tabPane.requestFocus();
}

Следующий раунд: пусть tabPane передает фокус, когда он фокусируется во время изменения выделения. Одно предложение, представляющее два камня преткновения:

  • нет API-интерфейса publi c для поддержки фокуса переноса, его нужно взломать, например, вручную запустив TAB
  • во время изменения выбора требуется лог состояния c, чтобы определить его начало и конец

В общем, выглядит как задача для пользовательского скина, который обрисован в общих чертах (будьте осторожны: официально не тестируется!) в Пример ниже (это для fx11, fx8 может быть похожим, но требует доступа к внутренним классам, потому что скины еще не опубликованы c)

public class TabPaneFocusOnSelectionSO extends Application {

    /**
     * Custom skin that tries to focus the first child of selected tab when 
     * selection changed.
     * 
     */
    public static class MyTabPaneSkin extends TabPaneSkin {

        private boolean selecting = true;
        /**
         * @param control
         */
        public MyTabPaneSkin(TabPane control) {
            super(control);
            // TBD: dynamic update on changing tabs at runtime
            addTabContentVisibilityListener(getChildren());
            registerChangeListener(control.focusedProperty(), this::focusChanged);
            registerChangeListener(control.getSelectionModel().selectedItemProperty(), e -> {
                selecting = true;
            });
        }

        /**
         * Callback from listener to skinnable's focusedProperty.
         * 
         * @param focusedProperty the property that's changed
         */
        protected void focusChanged(ObservableValue focusedProperty) {
            if (getSkinnable().isFocused() && selecting) {
                transferFocus();
                selecting = false;
            }
        }

        /**
         * Callback from listener to tab visibility.
         * 
         * @param visibleProperty the property that's changed 
         */ 
        protected void tabVisibilityChanged(ObservableValue visibleProperty) {
            BooleanProperty b = (BooleanProperty) visibleProperty;
            if (b.get()) {
                transferFocus();
            }
        }

        /**
         * No public api to transfer focus "away" from any node, hack by firing
         * a TAB key on the TabPane.
         */
        protected void transferFocus() {
            final KeyEvent tabEvent = new KeyEvent(KeyEvent.KEY_PRESSED, "", "",
                    KeyCode.TAB, false, false, false, false);
            Event.fireEvent(getSkinnable(), tabEvent);
        }

        /**
         * Register the visibilityListener to each child in the given list that 
         * is a TabContentArea.
         * 
         */
        protected void addTabContentVisibilityListener(List<? extends Node> children) {
            children.forEach(node -> {
                if (node.getStyleClass().contains("tab-content-area")) {
                    registerChangeListener(node.visibleProperty(), this::tabVisibilityChanged);
                }
            });
        }

    }

    private TabPane tabPane;

    private Parent createContent() {
        tabPane = new TabPane() {

            @Override
            protected Skin<?> createDefaultSkin() {
                return new MyTabPaneSkin(this);
            }

        };
        for (int i = 0; i < 3; i++) {
            VBox tabContent = new VBox();
            tabContent.getChildren().addAll(new Button("dummy " +i), new TextField("just a field " + i));
            Tab tab = new Tab("Tab " + i, tabContent);
            tabPane.getTabs().add(tab);
        }
        tabPane.getTabs().add(new Tab("no content"));
        tabPane.getTabs().add(new Tab("not focusable content", new Label("me!")));

        BorderPane content = new BorderPane(tabPane);
        return content;

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setTitle(" TabPane with custom skin ");
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}
...