Хотя не удивительно, что 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);
}
}