JavaFX: Issue with setting same Node to multiple tabs - tabs

I am trying to set a particular node to more than one tabs on a tab pane. The problem is that only the last tab has the node when the application is launched but the rest of the tabs are shown empty.
I am attaching the code and a few screen shots to explain the problem:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class TabExample extends Application
{
public static void main(String[] args)
{
Application.launch(args);
}
#Override
public void start(Stage primaryStage)
{
primaryStage.setTitle("Tabs");
Group root = new Group();
Scene scene = new Scene(root, 400, 250, Color.WHITE);
TabPane tabPane = new TabPane();
BorderPane borderPane = new BorderPane();
Text myText = new Text("Hello");
for (int i = 0; i < 5; i++)
{
Tab tab = new Tab();
tab.setText("Tab"
+ i);
HBox hbox = new HBox();
hbox.getChildren().add(new Label("Tab"
+ i));
hbox.setAlignment(Pos.CENTER);
tab.setContent(myText);
tab.setClosable(false);
tabPane.getTabs().add(tab);
}
tabPane.setSide(Side.BOTTOM);
// bind to take available space
borderPane.prefHeightProperty().bind(scene.heightProperty());
borderPane.prefWidthProperty().bind(scene.widthProperty());
borderPane.setCenter(tabPane);
root.getChildren().add(borderPane);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Please let me know if there is anything wrong with what I am doing or is it a known bug?

In JavaFX, each Node can have exactly one (1) Parent. See the Node Class Page in the JavaFX API. If you add a Node to another Parent, the Node looses its "connection" with the old Parent, which means it will not be displayed or accessable under the old Parent.
The method which causes this in your source code is tab.setContent(myText);.
To solve your problem, you have to create five different (=separate) objects and set each one of them to exactly one TabPane as child // content .

It is true that in JavaFx each node must have a single parent at any instant and that is the problem that you are facing.
However, there is a possible workaround using the onSelectionChanged() event of the tab to set the node as the child of this tab using setContent(), however make sure to check that the tab has changed to prevent duplicate child error here is a code snippet:
Tab currentTab;
Label label = new Label("Hello");
for(int i = 0;i < tabsNumber; i++){
Tab tab = new Tab();
currentTab = tab;
tab.setOnSelectionChanged(new EventHandler<Event>() {
#Override
public void handle(Event event) {
if (currentTab.equals(tab)) {
currentTab = tab;
currentTab.setContent(label);
}
}
});
tabPane.getTabs().add(tab);
}
Also if you are using scene builder or some other drag and drop tool you can do the same using a controller class just remember to keep track when the tab really changes.

Related

JavaFX walk widget tree

Is there a simple (uniform) way to recursively descend a JavaFX widget tree starting from a defined node (possibly from the Scene itself)?
The following code:
static class Visitor {
public void visit(Node node){
...
}
}
protected void walkWidgets(Node n, Visitor cb) {
if (n instanceof Parent) {
Parent p = (Parent) n;
for (Node c : p.getChildrenUnmodifiable()) {
walkWidgets(c, cb);
}
}
cb.visit(n);
}
... does not work because the "children" of some containers (e.g.: SplitPane, BorderPane, etc.) are not listed in their children Property.
To overcome this I should specialize the code to allow for all the quirks of all different widgets. This is particularly annoying when You start using widget libs beyond the "standard" provision.
Am I missing something? (I surely hope so!)
This seems to work fine: it gets all the child nodes in the BorderPane, SplitPane and TabPane.
import java.util.function.Consumer;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.SplitPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class WalkComponentTree extends Application {
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
root.setTop(new Label("Title"));
SplitPane splitPane = new SplitPane();
root.setCenter(splitPane);
ListView<String> list = new ListView<>();
list.getItems().addAll("One", "Two", "Three");
TabPane tabPane = new TabPane();
Tab tab1 = new Tab();
tab1.setContent(new TextArea());
Tab tab2 = new Tab();
tab2.setContent(new Label("Tab 2"));
tabPane.getTabs().addAll(tab1, tab2);
splitPane.getItems().addAll(tabPane, list);
Button button = new Button("Walk tree");
button.setOnAction(event -> walkTree(root, node ->
System.out.println(node.getClass())));
root.setBottom(button);
Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private void walkTree(Node node, Consumer<Node> visitor) {
visitor.accept(node);
if (node instanceof Parent) {
((Parent) node).getChildrenUnmodifiable()
.forEach(n -> walkTree(n, visitor));
}
}
public static void main(String[] args) {
launch(args);
}
}
Note you can also use node.lookupAll("*");, though this is less robust as it only works once css has been applied to the node.
In my opinion you are correct to say that there is no "standard" way to walk through the JavaFX scene graph.
One reason is, that Tab is not derived from Node, so you will not get a List of Tabs from Parent.getChildrenUnmodifiable(). Another reason is, that ScrollPane and TitledPane do not publish their content node in Parent.getChildrenUnmodifiable(). Some other Panes like Accordion, SplitPane and Toolbar also don't publish their children with Parent.getChildrenUnmodifiable(). So you need to handle this yourself.
For more Information please see:
Walking the JavaFX Scene Graph

Javafx 2.0 - Get ContextMenu Parent Node in EventHandler

I am writing a javafx UI and would like to get the owner Node of a contextMenu from a eventHandler for the MenuItem that was clicked on.
My Code:
TabPane tabPane = new TabPane();
Tab tab1 = new Tab();
Tab tab2 = new Tab();
tabPane.getTabs().addAll(tab1,tab2);
ContextMenu contextMenu = new ContextMenu();
MenuItem menuItem = new MenuItem("Do Some Action");
menuItem.setOnAction(new EventHandler<ActionEvent>(){
#override
public void handle(ActionEvent e){
// Get the tab which was clicked on and do stuffs with it
}
});
contextMenu.getItems().add(menuItem);
for(Tab tab: tabPane.getTabs()){
tab.setContextMenu(contextMenu);
}
What I would like to do is get a reference to the tab that had it's contextMenu selected.
I was able to get a reference to what appears to be the ContextMenu of the MenuItem with the following code inside of the handle(ActionEvent e) method for the menuItem eventHandler:
ContextMenu menu = ((ContextMenu)((MenuItem)e.getSource()).getParentPopup());
My idea from there was to use ContextMenu's .getOwnerNode() method on menu and then have a reference to the tab, but when running that I get a reference to an item that I can't make sense of.
The toString() method for the object returned by .getOwnerNode() returns "TabPaneSkin$TabHeaderSkin$3#14f59cef" which I can't figure out the meaning of.
Is my approach of trying to work my way up the chain until I get to the node correct or is there an entirely different approach that will work better?
All I need to have is the functionality of a ContextMenu, and when the MenuItem is clicked on, I need to have a reference to the tab for which the ContextMenu was selected so I can do cool stuffs with it.
Create a ContextMenu for each tab.
Make each tab final.
Directly reference the final tab in the menu item event handler for the context menu.
Here is a code snippet:
final Tab tab = new Tab("xyzzy");
ContextMenu contextMenu = new ContextMenu();
MenuItem menuItem = new MenuItem("Do Some Action");
menuItem.setOnAction(new EventHandler<ActionEvent>(){
#Override public void handle(ActionEvent e){
tab.setText("Activated by User");
}
});
Every time the user right clicks on a tab header and selects the "Count Click" menu, the content of the related tab is updated with a count of the number of licks counted so far for that tab.
Here is an executable sample:
import javafx.application.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;
public class TabContext extends Application {
#Override public void start(Stage stage) {
TabPane tabPane = new TabPane();
tabPane.getTabs().addAll(
createTab("xyzzy", "aliceblue"),
createTab("frobozz", "blanchedalmond")
);
stage.setScene(new Scene(tabPane));
stage.show();
}
private Tab createTab(String tabName, String webColor) {
final Label content = new Label("0");
content.setAlignment(Pos.CENTER);
content.setPrefSize(200, 100);
content.setStyle("-fx-font-size: 30px; -fx-background-color: " + webColor + ";");
final Tab tab = new Tab(tabName);
tab.setContent(content);
ContextMenu contextMenu = new ContextMenu();
MenuItem menuItem = new MenuItem("Count Click");
menuItem.setOnAction(new EventHandler<ActionEvent>(){
#Override public void handle(ActionEvent e){
content.setText(
"" + (Integer.parseInt(content.getText()) + 1)
);
}
});
contextMenu.getItems().add(menuItem);
tab.setContextMenu(contextMenu);
return tab;
}
public static void main(String[] args) { Application.launch(args); }
}
Alternately to using an anonymous inner class this way, you could create an EventHandler subclass with a constructor that includes the Tab for which the EventHandler is attached.
class TabContextMenuHandler implements EventHandler<ActionEvent> {
private final Tab tab;
TabContextMenuHandler(Tab tab) {
this.tab = tab;
}
#Override public void handle(ActionEvent event) {
tab.setText("Activated by User");
}
}

How to listen for close in a JPanel

I am working with some strange legacy code. They have a custom object which implements a JPanel. This JPanel object is a secondary popup screen within the main application. The issue I'm having is to detect when the secondary popup screen is closed.
I tried to implement a WindowListener for the class, but when I try to add it, there is no JFrame associated with this object. I am assuming this is because they are using a custom object and it is an embedded popup screen.
I tried to retrieve a JFrame using:
JFrame parentFrame = (JFrame) SwingUtilities.getWindowAncestor(this);
which fails on a NullPointerException. I have no idea why it's so difficult to detect the right hand corner "x" close button on this page! I should mention that they were able to add Mouse and Key Listeners to the table which is embedded within the JPanel. But the outside listener for the entire window is causing me troubles.
(Please bear with me, this is my first stackoverflow post and I am new to Swing.)
Thanks so very much!!
Try to call getParent() for that strange panel. It should return the parent GUI component. If this is still not your frame but some intermediate panel instead, call getParent() on it as well. The top level component returns null.
Component p = strangePanel;
while ( p != null && ! (p instanceof Window))
p = p.getParent();
((Window) p ).addWindowListener(..);
Cannot understand why you are getting "NullPointerException" at:
JFrame parentFrame = (JFrame) SwingUtilities.getWindowAncestor(this);
In two cases this can happen:
JFrame parentFrame = (JFrame) SwingUtilities.getWindowAncestor(null);
In your case, this is not possible as you have used this as a parameter.
Second, are you doing some other operations in above code line, like:
JFrame parentFrame = ((JFrame) SwingUtilities.getWindowAncestor(this)).someOperation();
In this case, if your this object represent the top window then you are supposed to get "NullPointerException" because ancestor of top parent is returned as "null". In other cases, I suspect you will get this exception.
Can you post a block of code where you are getting exception.
For this answer I'm making a minor assumption that the Nullpointer is not being thrown at the line that you mentioned, but rather when you attempt to add the WindowListener to the parentFrame. This is most likely because you're calling
JFrame parentFrame = (JFrame) SwingUtilities.getWindowAncestor(this);
before the JPanel has been added to the JFrame hierarchy.
Here's a rought code sample on how you could work around this. The thought it to wait for the panel to be notified that it has been attached to the JFrame somewhere in its hierarchy.
package test;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class HierarchyTest extends JPanel {
protected static void loadApp() {
HierarchyTest test = new HierarchyTest();
JFrame frame = new JFrame();
frame.add(test);
frame.setSize(200, 200);
frame.setVisible(true);
}
/**
* #param args
*/
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
loadApp();
}
});
}
public HierarchyTest() {
this.addHierarchyListener(new HierarchyListener() {
#Override
public void hierarchyChanged(HierarchyEvent e) {
// This can be optimized by checking the right flags, but I leave that up to you to look into
boolean connected = setupListenersWhenConnected();
if (connected) {
HierarchyTest.this.removeHierarchyListener(this);
}
}
});
}
protected boolean setupListenersWhenConnected() {
JFrame parentFrame = (JFrame) SwingUtilities.getWindowAncestor(this);
if (parentFrame == null) {
return false;
}
parentFrame.addWindowListener(new WindowAdapter() {
#Override
public void windowClosing(WindowEvent e) {
// Implementation here
System.out.println("This window is closing!");
}
});
return true;
}
}

Closing another JFrame from another method

I've been working on this for some time, and I'd really appreciate some help right now.
I'm trying to get the JFrame containing the text input fields to close from my actionPerformed method, but I can't seem to get anything to work. JFrame.dispose wont let me access the right Jframe, and setVisible(false) is equally useless, unless I'm doing this completely wrong.
//halp
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
class PersonInput extends JPanel
implements ActionListener {
//Fields for data entry
private JFormattedTextField firstField, lastField, dateField;
public String x[] = new String[3];
public PersonInput() {
//Values for the fields
String first = "First Name";
String last = "Last Name";
String date = "MM/DD/YYYY";
//Create the text fields and set them up.
firstField = new JFormattedTextField();
firstField.setValue(new String(first));
lastField = new JFormattedTextField();
lastField.setValue(new String(last));
dateField = new JFormattedTextField();
dateField.setValue(new String(date));
dateField.setColumns(10);
JButton ok = new JButton("OK");
ok.setVerticalTextPosition(AbstractButton.BOTTOM);
ok.setHorizontalTextPosition(AbstractButton.CENTER);
ok.setActionCommand("ok");
ok.addActionListener(this);
ok.setToolTipText("Confirms user input and continues with the program.");
JPanel buttons = new JPanel(new GridLayout(0,1));
buttons.add(ok);
//Layout the text fields in a panel.
JPanel fieldPane = new JPanel(new GridLayout(0,1));
fieldPane.add(firstField);
fieldPane.add(lastField);
fieldPane.add(dateField);
//Put the panels in this panel, labels on left,
//text fields on right.
setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
add(fieldPane, BorderLayout.CENTER);
add(buttons, BorderLayout.LINE_END);
}
public void actionPerformed(ActionEvent e) {
if ("ok".equals(e.getActionCommand()))
{
JFrame frame1 = new JFrame("People Sorter");
x[0] = firstField.getText();
x[1] = lastField.getText();
x[2] = dateField.getText();
JOptionPane.showMessageDialog(frame1, "Person has been added.");
dispPerson();
frame.setVisible(false);
}
}
public void dispPerson()
{
System.out.println(x[0] + x[1] + x[2]);
}
public static void createAndShowGUI() {
//Create and set up the window.
JFrame frame = new JFrame("Person Input");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//Add contents to the window.
frame.add(new PersonInput());
//Display the window.
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
//Turn off metal's use of bold fonts
UIManager.put("swing.boldMetal", Boolean.FALSE);
createAndShowGUI();
}
});
}
}
I'm all ears if anyone has any ideas; I've been stressed over this all day. Thanks much for lending me your time!
EDIT: Just for clarification, the frame I'm trying to close is the one instantiated in the createAndShowGUI method.
it seems that the problem is that we are trying to merge both static and non static contents. For a short explanation static contents can be referred without need of creating an instance (object) of that class. Which means that createAndShowGUI can be called:
inside another static method (like main)
From class reference PersonInput.createAndShowGUI()
or from an object, but that method or attribute will be always the same, static attributes are shared.
I can suggest 2 ways to solve your problem.
One is pass the object frame to PersonInput
//halp
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
class PersonInput extends JPanel
implements ActionListener {
//Fields for data entry
private JFormattedTextField firstField, lastField, dateField;
public String x[] = new String[3];
JFrame frame;
public PersonInput(JFrame frame) {
this.frame = frame;
//the rest of your code
}
The other way is to have the frame object outside the method and declare it static.
static JFrame frame = new JFrame("Person Input");;
public static void createAndShowGUI() {
//Create and set up the window.
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//Add contents to the window.
frame.add(new PersonInput());
//Display the window.
frame.pack();
frame.setVisible(true);
}
Remember that static variable cannot be referenced from a static context

TreeModelListener not responding to changes in TreeModel which it subscribes to

I'm having some bother understanding why I cannot get a TreeModelChanged listener to respond to changes in the model which it subscribes to.
I have managed to reproduce the problem in a small example.
The SysOut message does not print to the console whenever a new node is added to the tree.
I intend to replace the SysOut message with some commands to redraw the tree. At the moment I am using a SysOut message just to prove that the listener is not being fired.
Am I missing something fundamental?
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
public class TreeTest {
private JTree t;
private DefaultTreeModel m ;
public static void main(String[] args) {
new TreeTest();
}
public TreeTest() {
//Draw Frame & Panel - set dimensions
JFrame f = new JFrame();
f.setSize(new Dimension(800,600));
JPanel p = new JPanel();
p.setSize(new Dimension(800,600));
//Create a Tree Model. Give it a String at the root.
m = new DefaultTreeModel(new DefaultMutableTreeNode("Root"));
//Create a tree and add the Model to it
t = new JTree();
t.setModel(m);
//Try a Tree Model Listener
m.addTreeModelListener(new TreeModelListener() {
private void doSomething() {
//Should fire whenever a node is added to the model
System.out.println("Responding to TreeModelListener");
}
#Override
public void treeStructureChanged(TreeModelEvent e) {
doSomething();
}
#Override
public void treeNodesRemoved(TreeModelEvent e) {
doSomething();
}
#Override
public void treeNodesInserted(TreeModelEvent e) {
doSomething();
}
#Override
public void treeNodesChanged(TreeModelEvent e) {
doSomething();
}
});
//Add listener to a button which adds nodes to the tree when clicked
JButton addNode = new JButton("Add node");
addNode.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
DefaultMutableTreeNode newNode = new DefaultMutableTreeNode("New Node");
DefaultMutableTreeNode root = (DefaultMutableTreeNode) m.getRoot();
root.add(newNode);
}
});
JScrollPane s = new JScrollPane(t);
p.add(s);
p.add(addNode);
p.setVisible(true);
f.add(p);
f.setVisible(true);
}
}
that's because the model doesn't know about the addition, it happens under its feet. Use the methods on DefaultTreeModel to do the insertion:
model.insertNodeInto(newNode, root, root.getChildCount())
Edit
a TreeNode is just a (more or less) dumb data structure. As you can see in the api, it's not an Observable, so there is no way for the model which uses that data structure to detect if anything changed on the node. To make it aware of the change, you have to do one of two things
use the node manipulation methods of the model
update the node and notify the model manually (calling nodesWereInserted)
The first is the preferable way (keeps control where it belongs), the second might be needed in more complex contexts (though I would strongly recommend to never do it, that's why SwingX DefaultTreeTableModel doesn't have them exposed :)