Accessing Arbitrary nodes from EventHandler target - java

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!

Related

Setting the alignment of a node without using BorderPane?

So I have a Text node which I want to position at the Top-Right side of the scene. Most methods state to use BorderPane, but in my case some of the other nodes in my scene are using Panewhich is also my root pane, so the obvious workaround is to add the nodes you want to the BorderPane and add the BorderPane to the root Pane.
However BorderPane does not seem to be compatible with the Pane. That is, all the nodes of the BorderPane won't be shown. Also positioning the nodes based on their translate X and Y are not supported in BorderPane as well.(i.e. setLayoutX() does not work.) Because of this I cannot even make my root Pane to BorderPane.
Here is what I've tried:
public class foo {
private Pane root = new Pane();
private BorderPane borderPane = new BorderPane();
private Scene scene = new Scene(root, 1366, 768);
public foo(){
Text text1 = new Text("test1");
text1.setLayoutX(11); // Ignore this if you want.
test1.setLayoutY(11);
Text text2 = new Text("test2");
root.getChildren().add(test1);
borderPane.setRight(text2);
root.getChildren().add(borderPane);
// Display the scene on a stage.
}
}
Here only test1 is shown and test2 is not seen.
Is there a way I can position this text on the RIGHT TOP side of the screen without using BorderPane?
Pane just resizes the children to the preferred size. Since you only place the Text node as child of the BorderPane, its preferred size is the size of the child.
In this case I recommend using a different layout, e.g.
StackPane
This allows you set the alignment of a node within the layout using StackPane.setAlignment(child, newAlignment);. The drawback is that setting an absolute position requires you to either make the child unmanaged (with some side effects) or specifying 4 values (an Insets instance) as margin.
Text text1 = new Text("test1");
StackPane.setMargin(text1, new Insets(11, 0, 0, 11));
StackPane.setAlignment(text1, Pos.TOP_LEFT);
Text text2 = new Text("test2");
StackPane.setAlignment(text2, Pos.TOP_RIGHT);
root = new StackPane(text1, text2);
AnchorPane
This does not allow for center alignments, but it allows you to set absolute positions using layoutX and layoutY just the way you'd do in a Pane. You can set anchor properties to specify the distance of a child from the top, left, right and/or bottom:
Text text1 = new Text("test1");
text1.setLayoutX(11);
text1.setLayoutY(11);
Text text2 = new Text("test2");
AnchorPane.setRightAnchor(text2, 0d);
AnchorPane.setTopAnchor(text2, 0d);
root = new AnchorPane(text1, text2);

How to clone a node in the scene graph in JavaFX?

I have a HBox with prefHeight = 70 // no prefWidth or any width...
I also have a Pane with prefWidth = 50 // no prefHeight or any height...
I just want to add multiple instance of the pane to the HBox using some loop.
When I do add(pane) in the loop body it gives following error.
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Children: duplicate children added: parent = HBox[id=myHBox]
I need to find way to clone the pane(as it passes by value).
Can anybody help me please?
(taking snapshot are not work for me because prefHeight of the pane is not set/ computed using parent)
This error happens because you're trying to add the same instance of a Node to another Node. If you remove the comments from the example below you'll get that error as well. Loop, on the other hand, will work fine because in each iteration new Button instance is created.
#Override
public void start(Stage stage) {
FlowPane root = new FlowPane();
// Results in error
// Button b1 = new Button("Button");
// root.getChildren().addAll(b1,b1);
for (int i = 0; i < 4; i++) {
Button b = new Button("Button");
root.getChildren().add(b);
}
Scene scene = new Scene(root, 50, 100);
stage.setScene(scene);
stage.show();
}
Your pane is probably more complicated, but you have to use the same principle. Put the code responsible for creating your pane in a separate method, getPane() or such, and use it in a loop to obtain new instances.
JavaFX doesn't give you an out-of-the-box solution to make a deep copy of a Node. If your Node is crated statically you can:
Put the code responsible for creating it in a separate method and
use it throughtout your application every time you need to get a new
instance of your pane.
Define it in a FXML file and load it every time you need a new instance.
Things get significantly worse if your Node has properties or children that were created or modified dynamically by the user. In that case you have to inspect its elements and recreate them on your own.

Set tooltip on TableColumn(JavaFX) without side effect

There is a way to set tooltip like that
if (tooltipText != null) {
Label lb = new Label(column.getText());
lb.setTooltip(new Tooltip(tooltipText));
column.setText(null);
column.setGraphic(lb);
}
Unfortunately then will be exists ugly side-effect.
We set null to text of column but menuItems invoke column.getText(). If we don't do this, there will be double name in header.How to solve it? Suppose by means of css..
This answer doesn't erase side-effect
How to add a tooltip to a TableView header cell in JavaFX 8
Note: The following code relies on internals of the TableView skin. These could be subject to change, since they reside in the com.sun packages.
CSS lookup can be used to get access to the TableColumnHeader nodes after the first layout pass on the TableView. Those can be used to retrieve the Label that is used to display the TableColumn's text property.
#Override
public void start(Stage primaryStage) {
TableView tv = createTableView();
Scene scene = new Scene(tv);
// generate layout pass
tv.applyCss();
tv.layout();
// assign tooltips to headers
tv.lookupAll(".column-header").stream().forEach(n -> {
TableColumnHeader header = (TableColumnHeader) n;
Tooltip tooltip = new Tooltip("Tooltip: " + header.getTableColumn().getText());
((Control) header.lookup(".label")).setTooltip(tooltip);
});
primaryStage.setScene(scene);
primaryStage.show();
}

JavaFX - Dealing with duplicate children error - RadioButtons

I have three radio buttons, I want my GUI to be uploaded depend on the checkbox I click.
I had referred to this guide: http://docs.oracle.com/javafx/2/ui_controls/radio-button.htm
which seems ideal for my needs. However, it cannot recognize the type despite importing the correct libraries. I decided to write my own little code.
We have horizontal boxes and vertical boxes. I have added the labels and text elements into one horizontal box (indicated by hb, hb1, hb2..) and clubs them all together in one vertical box.
Here is my code for event listener for my radio button:
test1.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
try{
//Remove all existing children
vb.getChildren().removeAll(hb,hb2,hb3,hb4,hb5);
log("Cleared");
} catch(Exception e)
{
log(e.getMessage());
}
try{
//Add email and email text into a horizontal box
hb.getChildren().addAll(email, email_text);
hb.setSpacing(10);
//Added the box to the vertical box
vb.getChildren().add(hb);
log("Added the email input box");
}
catch(Exception e){
log(e.getMessage());
}
}
});
Declarations:
final ToggleGroup group = new ToggleGroup();
final RadioButton test1 = new RadioButton("Test 1");
test1.setToggleGroup(group);
grid.add(test1, 0, 1);
final RadioButton test2 = new RadioButton("Test 2");
test2.setToggleGroup(group);
grid.add(test2, 0, 2);
final VBox vb = new VBox(10); // main container
final HBox hb = new HBox(); // Email , Email Text
final HBox hb2 = new HBox(); // Corporate , Corporate Name
I am getting an error : Children: duplicate children added: parent = HBox#359889fd. I did a little research on this : java.lang.IllegalArgumentException: Children: duplicate children added: parent = VBox#872be7 . Honestly, even if we both are working on the same issue, I am having a real hard time getting his code.
My main issue: If I am removing ALL the element in the beginning, why is there a duplicate error?
Perhaps, my code may be completely wrong and made no sense. Do you recommend any other approach?
The main issue was I was removing the vb box but I was not removing the contents of hb. Whereas, I am adding hb. I should have also removed from hb.

JavaFX: How to change the focus traversal policy?

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 :)

Categories

Resources