JavaFX walk widget tree - widget

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

Related

JavaFX: Issue with setting same Node to multiple 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.

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;
}
}

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

Setting an icon for a jFrame in Netbeans swing gui builder

I've been trying to set up a window in Netbean's GUI builder, without success. I've tried accessing the JFrame, from my main class as:
public void run(){
JFrame frame = new JFrame("Title of Frame");
frame.setIconImage(Toolkit.getDefaultToolkit().getImage("org/icon.png"));
}
Which creates a new frame apart from my main window with my icon.png. I'd like to know if there is some way to gain access to the JFrame that contains the rest of my UI elements, and set that icon.
I've also tried
new SearchAppUI().setIconImage(null);
which doesn't do anything of note.
Setting the icon directly:
JFrame.setIconImage("org/icon.png");
gives me the error, non-static method setIconImage(java.awt.Image) cannot be referenced from a static context.
Is there any way to set the main JFrame's icon from either Netbean's swing desinger preview, or from my run() method?
The OP is a bit dated but just for the record, try this:
frame.setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("org/icon.png")));
NVM, I found a solution on: http://www.youtube.com/watch?v=o_35iro4b7M
Describing how to set the icon and title of a jFrame. Basically, it requires
the libraries
import javax.swing.JFrame;
import java.awt.image.BufferedImage;
import java.io.File;
import java.awt.Image;
import javax.imageio.ImageIO;
I pretty much wanted to stick with using Netbean's guibuilder for now, at least for prototyping.
First of all. It's worth to read How to Make Frames.
You can use the following example.
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class FrameWithIcon extends JFrame {
public static void main(String[] args) {
try {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
FrameWithIcon myFrame = new FrameWithIcon();
myFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
myFrame.setTitle("Frame with Icon");
myFrame.setLayout(new BorderLayout());
myFrame.setIconImage(
loadImageIcon("org/icon.png").getImage());
Dimension size = new Dimension(250, 100);
JPanel panel = new JPanel();
panel.setPreferredSize(size);
myFrame.add(panel, BorderLayout.LINE_START);
myFrame.setVisible(true);
myFrame.pack();
}
});
} catch (InterruptedException ex) {
} catch (InvocationTargetException ex) {
}
}
/** Returns an ImageIcon, or null if the path was invalid. */
private static ImageIcon loadImageIcon(String path) {
URL imgURL = FrameWithIcon.class.getResource(path);
if (imgURL != null) {
return new ImageIcon(imgURL);
} else {
System.err.println("Couldn't find file: " + path);
return null;
}
}
}

Clojure defrecord and private fields

I frequently implement my Java swing GUIs using Martin Fowler's Presentation Model pattern.
Here is an example:
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListModel;
interface MainView {
void configurationButtonAddActionListener(ActionListener actionListener);
void directoryLabelSetText(String text);
ListModel fileListGetModel();
void setVisible(final boolean visible);
}
class MainFrame
extends JFrame
implements MainView {
private final JButton configurationButton = new JButton("Configuration...");
private final JLabel directoryLabel = new JLabel();
private final JList fileList = new JList();
public MainFrame(final String title) {
super(title);
final JPanel mainPanel = new JPanel(new BorderLayout());
add(mainPanel);
mainPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
mainPanel.add(directoryLabel, BorderLayout.NORTH);
mainPanel.add(new JScrollPane(fileList));
mainPanel.add(configurationButton, BorderLayout.SOUTH);
setSize(800, 600);
setLocationRelativeTo(null);
}
#Override
public void configurationButtonAddActionListener(final ActionListener actionListener) {
configurationButton.addActionListener(actionListener);
}
#Override
public void directoryLabelSetText(final String text) {
directoryLabel.setText(text);
}
#Override
public ListModel fileListGetModel() {
return fileList.getModel();
}
}
The interface can then be passed to a presenter class that is responsible for handling all actions on the view. A mock version can be passed into the presenter for testing and the view is so simple that, in theory, it does not need to be unit tested.
I am trying to do something similar in Clojure using defrecord:
(ns mainframe
(:gen-class)
(:import
[java.awt BorderLayout]
[javax.swing JButton JFrame JLabel JList JPanel JScrollPane]))
(if *compile-files*
(set! *warn-on-reflection* true))
(defprotocol MainView
(directory-label-set-text [this text])
(set-visible [this visible]))
(defrecord mainframe [^JFrame frame
directory-label
file-list
configuration-button]
MainView
(directory-label-set-text [this text]
(.setText directory-label text))
(set-visible [this visible]
(.setVisible frame visible)))
(defn create-main-frame
[title]
(let [directory-label (JLabel.)
file-list (JList.)
configuration-button (JButton. "Configuration...")
main-panel (doto (JPanel. (BorderLayout.))
(.add directory-label BorderLayout/NORTH)
(.add (JScrollPane. file-list))
(.add configuration-button BorderLayout/SOUTH))
frame (doto (JFrame.)
(.setTitle title)
(.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE)
(.add main-panel)
(.setSize 800 600)
(.setLocationRelativeTo nil))]
(mainframe. frame directory-label file-list configuration-button)))
The only way I can up with to do the interface and "class" are using defprotocol and defrecord. Is there a better way? Is there any way to make the "fields" in the defrecord that contain the components (JButton, JLabel, JList) private? I dislike exposing the implementation details.
For these implementation kind of things you probably want deftype instead of defrecord. defrecord is more about data, while deftype is used to implement the nitty-gritty behind some interfaces. This sounds a bit fuzzy, I know, but it is my interpretation of http://clojure.org/datatypes. I think your frame falls into the second category.
I wouldn't spend too much time on trying to hide things. Don't touch a types fields (unless inside of an interface function). Use only interface functions to interact with the type. Then it doesn't matter whether the field is technically private or public. (Again: cf. http://clojure.org/datatypes, section about opinions)