JavaFX: How to change the focus traversal policy? - java

Is it possible in JavaFX to change the focus traversal policy, like in AWT?
Because the traversal order for two of my HBoxes is wrong.

The simplest solution is to edit the FXML file and reorder the containers appropriately. As an example, my current application has a registration dialog in which a serial number can be entered. There are 5 text fields for this purpose. For the focus to pass from one text field to the other correctly, I had to list them in this way:
<TextField fx:id="tfSerial1" layoutX="180.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial2" layoutX="257.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial3" layoutX="335.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial4" layoutX="412.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial5" layoutX="488.0" layoutY="166.0" prefWidth="55.0" />

Bluehair's answer is right, but you can do this even in JavaFX Scene Builder.
You have Hierarchy panel in left column. There are all your components from scene. Their order represents focus traversal order and it responds to their order in FXML file.
I found this tip on this webpage:www.wobblycogs.co.uk

In common case the navigation is done in a container order, in order of children, or according to arrow keys pressing. You can change order of nodes - it will be the optimal solution for you in this situation.
There is a back door in JFX about traversal engine strategy substitution :
you can subclass the internal class com.sun.javafx.scene.traversal.TraversalEngine
engine = new TraversalEngine(this, false) {
#Override public void trav(Node owner, Direction dir) {
// do whatever you want
}
};
And use
setImpl_traversalEngine(engine);
call to apply that engine.
You can observe the code of OpenJFX, to understand, how it works, and what you can do.
Be very careful : it is an internal API, and it is likely to change, possibly, in the nearest future. So don't rely on this (you cannot rely on this officialy, anyway).
Sample implementation :
public void start(Stage stage) throws Exception {
final VBox vb = new VBox();
final Button button1 = new Button("Button 1");
final Button button2 = new Button("Button 2");
final Button button3 = new Button("Button 3");
TraversalEngine engine = new TraversalEngine(vb, false) {
#Override
public void trav(Node node, Direction drctn) {
int index = vb.getChildren().indexOf(node);
switch (drctn) {
case DOWN:
case RIGHT:
case NEXT:
index++;
break;
case LEFT:
case PREVIOUS:
case UP:
index--;
}
if (index < 0) {
index = vb.getChildren().size() - 1;
}
index %= vb.getChildren().size();
System.out.println("Select <" + index + ">");
vb.getChildren().get(index).requestFocus();
}
};
vb.setImpl_traversalEngine(engine);
vb.getChildren().addAll(button1, button2, button3);
Scene scene = new Scene(vb);
stage.setScene(scene);
stage.show();
}
It will require strong analitical skills for common case ;)

We're using JavaFX event filters for this, e.g.:
cancelButton.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if (event.getCode() == KeyCode.TAB && event.isShiftDown()) {
event.consume();
getDetailsPane().requestFocus();
}
}
});
The event.consume() suppresses the default focus traversal, which otherwise causes trouble when calling requestFocus().

This is the accepted answer adapted to change of internal api (happened at some point of fx-8, my current version is 8u60b5). Obviously the original disclaimer still applies: it's internal api, open to change without notice at any time!
The changes (compared to the accepted answer)
Parent needs a TraversalEngine of type ParentTraversalEngine
nav is no longer a method of TraversalEngine (nor ParentTE) but only of TopLevelTraversalEngine
the navigation implementation is delegated to strategy called Algorithm
actual focus transfer is (seems to be?) handled by TopLevelTE, Algorithm only finds and returns the new target
The plain translation of the example code:
/**
* Requirement: configure focus traversal
* old question with old hack (using internal api):
* http://stackoverflow.com/q/15238928/203657
*
* New question (closed as duplicate by ... me ..)
* http://stackoverflow.com/q/30094080/203657
* Old hack doesn't work, change of internal api
* rewritten to new internal (sic!) api
*
*/
public class FocusTraversal extends Application {
private Parent getContent() {
final VBox vb = new VBox();
final Button button1 = new Button("Button 1");
final Button button2 = new Button("Button 2");
final Button button3 = new Button("Button 3");
Algorithm algo = new Algorithm() {
#Override
public Node select(Node node, Direction dir,
TraversalContext context) {
Node next = trav(node, dir);
return next;
}
/**
* Just for fun: implemented to invers reaction
*/
private Node trav(Node node, Direction drctn) {
int index = vb.getChildren().indexOf(node);
switch (drctn) {
case DOWN:
case RIGHT:
case NEXT:
case NEXT_IN_LINE:
index--;
break;
case LEFT:
case PREVIOUS:
case UP:
index++;
}
if (index < 0) {
index = vb.getChildren().size() - 1;
}
index %= vb.getChildren().size();
System.out.println("Select <" + index + ">");
return vb.getChildren().get(index);
}
#Override
public Node selectFirst(TraversalContext context) {
return vb.getChildren().get(0);
}
#Override
public Node selectLast(TraversalContext context) {
return vb.getChildren().get(vb.getChildren().size() - 1);
}
};
ParentTraversalEngine engine = new ParentTraversalEngine(vb, algo);
// internal api in fx8
// vb.setImpl_traversalEngine(engine);
// internal api since fx9
ParentHelper.setTraversalEngine(vb, engine);
vb.getChildren().addAll(button1, button2, button3);
return vb;
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

In scene builder go to the view menu and select show document. on the left will be all objects in your current fxml document. drag controls up or down in the list to reorder for tab index. Select hide document to use the other tools since the document pane hogs the space.

I used Eventfilter as solution in combination with referencing the field ID's, so all you have to do is name the fields like (field1,field2,field3,field4) so you can place the fields where u want:
mainScene.addEventFilter(KeyEvent.KEY_PRESSED, (event) -> {
if(event.getCode().equals(KeyCode.TAB)){
event.consume();
final Node node = mainScene.lookup("#field"+focusNumber);
if(node!=null){
node.requestFocus();
}
focusNumber ++;
if(focusNumber>11){
focusNumber=1;
}
}
});

You can do this easily with SceneBuilder. Open fxml file with scenebuilder and go to hierarchy under Document tab. Place input controls into a order you want by dragging input controls. Remember to check Focus Traversal in properties. Then focus traversal policy will work nicely when you press tab bar.

You can use NodeName.requestFocus() as said above; furthermore, make sure you request this focus after instantiating and adding all of your nodes for the root layout, because as you do so, the focus will be changing.

I had a problem in which a JavaFX Slider was capturing right and left arrow keystrokes that I wanted to be handled by my method keyEventHandler (which handled key events for the Scene). What worked for me was to add the following line to the code that initialized the Slider:
slider.setOnKeyPressed(keyEventHandler);
and to add
keyEvent.consume();
at the end of keyEventHandler.

General solution inspired by Patrick Eckert's answer.
When I am creating the UI, say for example adding a TextField, I set things up like so:
List<String> displayOrder;
Map<String, Node> cycle;
TextField tf = new TextField();
tf.setId("meTF");
cycle.put("meTF", tf);
displayOrder.add("meTF");
getChildren().add(tf);
Then on the UI element (layout usually) you add this:
ui.addEventFilter(KeyEvent.KEY_PRESSED, (ke) ->
{
if(!ke.getCode().equals(KeyCode.TAB) || !(ke.getTarget() instanceof Node))
return;
int i = displayOrder.indexOf(((Node)ke.getTarget()).getId());
if(i < 0) // can't find it
return;
if(ke.isShiftDown())
i = (i == 0 ? displayOrder.size() - 1 : i - 1);
else
i = ++i % displayOrder.size();
cycle.get(displayOrder.get(i)).requestFocus();
ke.consume();
});
FYI I think it is important to only consume the event if you are actually going to call request focus on something. Less likely to break something unintentionally that way...
If anyone can think of ways to optimize this further I'd appreciate knowing :)

Related

Accessing Arbitrary nodes from EventHandler target

I'm working on a small JavaFX application. In this application I have the following components :
BorderPane -> as the root element
HBox top, bottom -> top and bottom regions
VBox left, right -> left and right regions
FlowPane center -> central region
When the central region is clicked i need to access a node in the top region containing some text. In order to access it i climb the graph upwards from the event's target like this :
public EventHandler<MouseEvent> fieldClicked = (MouseEvent e) -> {
FlowPane target = (FlowPane)e.getTarget();
BorderPane root = (BorderPane)target.getParent();
HBox top = (HBox)root.getChildren().get(0);
HBox top_left = (HBox)top.getChildren().get(0);
Text total = (Text)top_left.getChildren().get(0);
ObservableList<Node> dices = target.getChildren();
/* Do some stuff with retrieved nodes */
};
Is there a better and less verbose way to access an arbitrary node in the scene graph beside iteratively calling Node.getParent()
If you do not store the field in some other way, no. You may attach some id to find a node via CSS selector (lookup) but in that case you're better of doing this a different way:
Store the nodes you need to access in fields (or effectively final local variables, if you register event handlers in the same scope where you create the nodes).
...
private BorderPane root;
private HBox top;
private Text total;
private FlowPane target;
public EventHandler<MouseEvent> fieldClicked = (MouseEvent e) -> {
ObservableList<Node> dices = target.getChildren();
/* Do some stuff with fields */
};
private void initializeNodes() {
...
total = new Text();
top = new HBox(total);
root.setTop(top);
target = new FlowPane();
root.setCenter(target);
...
}
It's better to decouple the modification of certain values from the layout of the scene as much as possible anyways, since this makes it easier for you to rearrange the scene without having to worry about event handlers navigating the scene correctly via up-/downward navigation though the scene. Furthermore you'll get into trouble, if you're using your approach in cases where you use a "parent" other than a Pane or Group, e.g. ScrollPane since the skin of ScrollPane inserts the content node into the scene as it's descendant, but not as it's child and it doesn't do this until the first layout pass.
BTW: Note that it's Event.getSource that yields the node the event handler was triggered for, not Event.getTarget.
To get a specific Node, you can use the lookup() method of the javafx.scene.Scene class.
For Example, you can set an ID on the node containing some text and then find it with scene.lookup("#theID");
public EventHandler<MouseEvent> fieldClicked = (MouseEvent e) -> {
FlowPane target = (FlowPane)e.getTarget();
Text total = (Text) target.getScene().lookup("#myTextID");
/* Do some stuff with retrieved nodes */
};
The ID you can set by:
Text text = new Text("My Text element somewhere");
text.setId("myTextID");
I'm new to JavaFX so I don't know if this is the best way either. But I hope, this is what you are looking for.
By the way, if you want to get to the root node, you can instead use:
public EventHandler<MouseEvent> fieldClicked = (MouseEvent e) -> {
FlowPane target = (FlowPane)e.getTarget();
BorderPane root = (BorderPane) target.getScene().getRoot();
};
Its maybe helpful when you have more Elements in your FlowPane, then you also don't have to call that often Node.getParent().
Hope it helps!

JavaFX TabPane tabs don't update position

I noticed that when adding and deleting tabs from a TabPane, it fails to match the position of the order of tabs in the underlying list. This only happens when at least one tab is hidden entirely due to the width of the parent. Here's some code that replicates the issue:
public class TabPaneTester extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Scene scene = sizeScene();
primaryStage.setMinHeight(200);
primaryStage.setWidth(475);
primaryStage.setScene(scene);
primaryStage.show();
}
private Scene sizeScene(){
TabPane tabPane = new TabPane();
tabPane.setTabMinWidth(200);
tabPane.getTabs().addAll(newTabs(3));
Scene scene = new Scene(tabPane);
scene.setOnKeyPressed(e -> tabPane.getTabs().add(1, tabPane.getTabs().remove(0)));
return scene;
}
private static Tab[] newTabs(int numTabs){
Tab[] tabs = new Tab[numTabs];
for(int i = 0; i < numTabs; i++) {
Label label = new Label("Tab Number " + (i + 1));
Tab tab = new Tab();
tab.setGraphic(label);
tabs[i] = tab;
}
return tabs;
}
public static void main(String[] args) {
launch();
}
}
When you press a key, it removes the first tab (at index 0) and puts it back at index 1, effectively swapping the first two tabs. However, when run the tabs don't actually visually swap (even though the tab switcher menu does switch their position).
If you change the width of the screen to include even a pixel of the third tab that was hidden (replace 475 with 500), it works as intended. Any clues as to how to fix this?
This is indeed a bug and I couldn't find it reported in the public JIRA it is now reported at https://bugs.openjdk.java.net/browse/JDK-8193495.
All my analysis is based on the code in TabPaneSkin if you want to have a look yourself.
Summary
The problem arises when you remove and then add the tab "too quickly". When a tab is removed, asynchronous calls are made during the removal process. If you make another change such as adding a tab before the async calls finish (or at least "finish enough"), then the change procedure sees the pane at an invalid state.
Details
Removing a tab calls removeTabs, which is outlined below:
Various internal removal methods are called.
Then it checks if closing should be animated.
If yes (GROW),
an animation queues a call to a requestLayout method, which itself is invoked asynchronously,
and the animations starts (asynchronously) and the method returns.
If not (NONE),
requestLayout is called immediately and the method returns.
The time during which the pane is at an invalid state is the time from when the call returns until requestLayout returns (on another thread). This duration is equivalent to the duration of requestLayout plus the duration of the animation (if there is one), which is ANIMATION_SPEED = 150[ms]. Invoking addTabs during this time can cause undesired effects because the data needed to properly add the tab is not ready yet.
Workaround
Add an artificial pause between the calls:
ObservableList<Tab> tabs = tabPane.getTabs();
PauseTransition p = new PauseTransition(Duration.millis(150 + 20));
scene.setOnKeyPressed(e -> {
Tab remove = tabs.remove(0);
p.setOnFinished(e2 -> tabs.add(1, remove));
p.play();
});
This is enough time for the asynchronous calls to return (don't call the KeyPressed handler too quickly in succession because you will remove the tabs faster than they can be added). You can turn off the removal animation with
tabPane.setStyle("-fx-close-tab-animation: NONE;");
which allows you to decrease the pause duration. On my machine 15 was safe (here you can also call the KeyPressed handler quickly in succession because of the short delay).
Possible fix
Some synchronization on tabHeaderArea.

ListView with custom CellFactory trims invisible nodes

My layout issue
I have a little issue with ListView and I'm not sure if it's because of some knowledge I missing or if my approach is flawed. Have to admit I'm not yet clear with how JavaFX handle the layout in the many possible cases.
The above screenshot shows the result I get twice with the exact same code, except that on the second one an invisible shape I use for coherent layout is made visible for debug.
The various classes involved by the CellFactory extend Group, I tried with some other Parent without much success so far.
How to reproduce
Rather than sharing my StarShape, StarRow and some other misc classes (I'd be happy to if requested) I wrote a sample reproducing the issue. The class extends Application and overrides the start(...) method as such:
#Override
public void start(Stage primaryStage) throws Exception {
final StackPane root = new StackPane();
final Scene scene = new Scene(root, 400, 600);
final ListView<Boolean> listView = new ListView<>();
listView.setCellFactory(this::cellFactory);
for (int i = 0; i < 5 ; i++) {
listView.getItems().add(true);
listView.getItems().add(false);
}
root.getChildren().add(listView);
primaryStage.setScene(scene);
primaryStage.setTitle("ListView trims the invisible");
primaryStage.show();
}
where this::cellFactory is
private ListCell<Boolean> cellFactory(ListView<Boolean> listView) {
return new ListCell<Boolean>() {
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
final Rectangle tabShape = new Rectangle();
tabShape.setHeight(20);
tabShape.setWidth(40);
tabShape.setVisible(item);
final Label label = new Label(item.toString());
label.setLayoutX(40);
final Group cellRoot = new Group();
cellRoot.getChildren().add(tabShape);
cellRoot.getChildren().add(label);
setGraphic(cellRoot);
}
}
};
}
The above will display a ListView<Boolean> with black shapes in front of true items (because of the tabShape.setVisible(item); bit). The false items are looking like regular Label objects as if the invisible shape in their Group wasn't there (but it is).
Closing comments
Debugging this, it turns out groups with the invisible shapes are given negative layoutX property values. Thus Label controls aren't aligned as I'd like them to be. It doesn't happen when I call setLayoutX and setLayoutY outside of a ListView (the invisible shapes do force offsets), but it's probably not the only place where it would happen.
What's happening and how to avoid it? Alternatively, as I'm guessing I'm approaching this wrong, what'd be the right way? In other words, what is the question I should be asking instead of this?
Taking from #dlatikay's comment, instead of setting the placeholder items to invisible, you can render them transparent by setting their opacity to 0.0.
Applied to the MCVE from your question, this would be done by replacing:
tabShape.setVisible(item);
with:
tabShape.setOpacity(item ? 1.0 : 0.0);
In terms of user experience, you could take this one step further. Instead of setting the "inactive" stars to fully transparent, you could set them to be near-transparent, as in this mockup (with opacity set to 0.1):
The benefits that I see are:
It indicates not only the rating of an item in the list, but also the maximum rating.
It avoids awkward empty spaces for list items with zero stars.
I'm guessing I'm approaching this wrong
No, you're not. As with all layouts, there's often multiple ways to approach the same problem. Your approach is actually correct, and you're very close to a working solution.
You can achieve what you're after with a mere 1 line change. That is, changing the Group to an HBox.
An HBox ensures that elements are ordered horizontally, one after another. They also allow invisible elements to still take up space.
I also commented out one line: label.setLayoutX(40). I did this because HBox will not respect this setting, and actually you don't need it to. It will automatically shift the elements horizontally by as much is required.
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
}
else {
final Rectangle tabShape = new Rectangle();
tabShape.setHeight(20);
tabShape.setWidth(40);
tabShape.setVisible(item);
final Label label = new Label(item.toString());
//label.setLayoutX(40);
final HBox cellRoot = new HBox();
cellRoot.getChildren().add(tabShape);
cellRoot.getChildren().add(label);
setGraphic(cellRoot);
}
}
When I make those changes, your layout will render like so:
Important: Your example and your screenshots are slightly different. You may want to use a VBox for your star example (V for 'vertical', H for 'horizontal').

Mimicking CTRL+Click Multiple Selection in ListView using Javafx

I'm trying to find different ways of selecting multiple items in a ListView. The GUI will be running on a touch screen monitor, So I won't be able to CTRL+Click. From researching through various past posts, I have been able to implement Multiple Selection via keeping all the selected items in an Array and then looping through it to get the final selections. The only problem I have with my code is that compared to a CTRL +click , the selection is done smoothly, where as my code leads to a type flickering every time a new item is selected. So basically the listView clears all the selections and then selects the correct ones. Is there a way to make this transition go smoothly? Would it be easier to mimic a touch to have CTRL+click effect?
selectedList = new int[totalTypes];//total number of item properties
for(int x=0; x<selectedList.length;x++){//0 = not selected, 1 = selected
selectedList[x]=0;
}
testView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
testView.setOnMouseClicked(new EventHandler<Event>(){
#Override
public void handle(Event event){
if(selectedList[testView.getSelectionModel().getSelectedIndex()]==0){
selectedList[testView.getSelectionModel().getSelectedIndex()]=1;
}
else{
selectedList[testView.getSelectionModel().getSelectedIndex()]=0;
}
for(int x=0; x<selectedList.length;x++){
if(selectedList[x]==1){
testView.getSelectionModel().select(x);
}
else{
testView.getSelectionModel().clearSelection(x);;
}
}
}
});
You could handle changing the selection when a user clicks a ListCell yourself instead of using the standard event handling:
#Override
public void start(Stage primaryStage) {
ListView<Integer> listView = new ListView<>();
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
listView.getItems().setAll(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
listView.addEventFilter(MouseEvent.MOUSE_PRESSED, evt -> {
Node node = evt.getPickResult().getIntersectedNode();
// go up from the target node until a list cell is found or it's clear
// it was not a cell that was clicked
while (node != null && node != listView && !(node instanceof ListCell)) {
node = node.getParent();
}
// if is part of a cell or the cell,
// handle event instead of using standard handling
if (node instanceof ListCell) {
// prevent further handling
evt.consume();
ListCell cell = (ListCell) node;
ListView lv = cell.getListView();
// focus the listview
lv.requestFocus();
if (!cell.isEmpty()) {
// handle selection for non-empty cells
int index = cell.getIndex();
if (cell.isSelected()) {
lv.getSelectionModel().clearSelection(index);
} else {
lv.getSelectionModel().select(index);
}
}
}
});
Scene scene = new Scene(listView);
primaryStage.setScene(scene);
primaryStage.show();
}

Get associated mouse click from ExpandTree event in SWT

I'm trying to stop the tree from collapsing or expanding when the user double clicks a column on a tree. It should only be allowed if the user clicks on the first column.
See, if a user double clicks the checkbox on node2 world1, the tree expands or collapses. I don't want that to happen. My tree needs SWT.FULL_SELECTION to detect the clicks on each of the columns, so that's not the way to go.
My listener looks like this
tree.addTreeListener(new TreeListener() {
#Override
public void treeExpanded(TreeEvent e) {
TreeItem parent = (TreeItem) e.item;
Point p = new Point (e.x, e.y);
int column = CheckboxClickListener.getColumn(p,parent);
if (column > 0) {
e.doit = false;
}
}
#Override
public void treeCollapsed(TreeEvent e) {
TreeItem parent = (TreeItem) e.item;
Point p = new Point (e.x, e.y);
int column = CheckboxClickListener.getColumn(p,parent);
if (column != 0) {
e.doit = false;
}
}
});
Problem is, the mouse event that generated the click is not the same as the TreeEvent that expands the tree. Thus, the e.x and e.y are both zero, making my Point detection useless. Listening to the mouse event and maintaining the last x and y to check here in the TreeExpand event seems bug-prone since the user may also expand the tree using the keyboard (thus the x and y may not reflect the user action). I also considered adding a time constraint to check that but seems like a bad way to handle the issue.
How can I detect which mouse event triggered the expand event?
PS: e.doit=false does nothing, even outside the if condition, so help with stopping the tree from expanding/collapsing would be appreciated as well :)
Thank you!
I found someone saying this is a bug at this link http://www.eclipse.org/forums/index.php/t/257325/
The following code stops the tree from expanding on doubleclick but I'm not sure why or what are the side effects.
tree.addListener (SWT.MeasureItem, new Listener(){
#Override
public void handleEvent(Event event) {}
});
This stops the expanding when doubleclicking ANY column. Clicking on the small arrow at the left of the TreeItem still expands the tree (as it should).

Categories

Resources