I have two *.fxml forms with controllers. First is Window, second - ProductPane.
Simplified Window.fxml is:
<BorderPane prefWidth="650.0" prefHeight="450.0" fx:controller="package.WindowController">
<center>
<TabPane fx:id="tabsPane">
<tabs>
<Tab fx:id="productsTab" text="Products"/>
<Tab fx:id="warehouseTab" text="Warehouse"/>
<Tab fx:id="saleTab" text="Sale"/>
</tabs>
</TabPane>
</center>
</BorderPane>
Controller for Window.fxml:
public class WindowController {
#FXML
private TabPane tabsPane;
#FXML
private Tab productsTab;
#FXML
void initialize() {
sout("Main Window initialization...");
tabsPane.getSelectionModel().selectedIndexProperty().addListener((e, o, n) -> {
sout("Changed to " + n);
});
tabsPane.getSelectionModel().selectedItemProperty().addListener((e, o, n) -> {
sout("New item: " + n);
// Load ProductPane content:
if(n == productsTab) {
try {
Parent p = FXMLLoader.load(getClass().getResource("productPane.fxml"));
n.setContent(p);
} catch(IOException ex) {
ex.printStackTrace();
}
}
});
sout("Select first item...");
tabsPane.getSelectionModel().selectFirst();
// This methods also don't work
// tabsPane.getSelectionModel().clearAndSelect();
// tabsPane.getSelectionModel().select(productTab);
// tabsPane.getSelectionModel().select(0);
}
}
The problem is: when I load Window.fxml in main() and launch it appearse window with empty first tab.
Debug output:
Main Window initialization...
Select first item...
But ProductPane not loaded and listener do not call. If I switch between tabs in Window, listeners are triggered and Products tab loaded properly.
What's the problem?
You added a ChangeListener to the tab pane's selection model, which of course gets notified when the selection changes. By default, the first tab is selected, so at the time the change listener is added, the first tab is already selected. This means when you call selectFirst(), the selection doesn't change (because you're asking to select the tab that is already selected), so the listener isn't notified.
The solution is a little ugly: you just need to directly load your products tab content if the products tab is selected at the time you add the listener. I would factor that code into a separate method to avoid too much repetition:
#FXML
void initialize() {
System.out.println("Main Window initialization...");
tabsPane.getSelectionModel().selectedIndexProperty().addListener((e, o, n) -> {
System.out.println("Changed to " + n);
});
tabsPane.getSelectionModel().selectedItemProperty().addListener((e, o, n) -> {
System.out.println("New item: " + n);
// Load ProductPane content:
if(n == productsTab) {
loadProductsTab();
}
});
if (tabPane.getSelectionModel().getSelectedItem() == productsTab) {
loadProductsTab();
}
}
private void loadProductsTab() {
try {
Parent p = FXMLLoader.load(getClass().getResource("productPane.fxml"));
productsTab.setContent(p);
} catch(IOException ex) {
ex.printStackTrace();
}
}
If you find you need this kind of functionality a lot, you might be interested in the ReactFX framework, which (I think) has built-in functionality that handles these kinds of cases.
Related
i am using kotlin to create some small application with it, and tornadofx, however i am unfamiliar with how some of the javafx things translate to kotlin.
I am trying to itterate through a list of buttons, and add button to page, but i managed only to add one button per page, and i want to have X items per page, and i want page count to be dynamic
override val root = tabpane{
setPrefSize(800.0,600.0)
tabs.removeAll()
tabClosingPolicy = TabPane.TabClosingPolicy.ALL_TABS
val buttonList = arrayOf(
Button("Btn1"),
Button("Btn2"),
Button("Btn3"),
Button("Btn4")
)
tab("My Tab 1") {
hbox {
button("Click Me!") {
setOnAction {
replaceWith<TestView>()
}
}
button("Add Tab 2") {
setOnAction {
tab("tab2 ")
{
select()
button("Close") { setOnAction { close() } }
pagination(buttonList.size) { setPageFactory { pageIndex -> (buttonList[pageIndex]) } }
style {
arrowsVisible = false
pageInformationVisible = false
}
}
}
}
}
}
}
i tryed to implement a for loop that would add certain amount of items per page (lets say 2 buttons per page) and then proceed to create page. but i had no luck.
If anyone could help me find a proper solution for kotlin based pagination i would be thankful
pagination(buttonList.size)
with this line i can controll amount of pages, so that is at least something, but i dont see how to control amount items per page.
pageIndex -> (buttonList[pageIndex])
i realize issue is here, but i had no luck implementing proper logic.
lets say i want 4 items per page, i could get amount of pages required by dividing my list with number 4 (items per page). but not sure how to add more then 1 item per page.
class SomeView : View() {
private val buttonList = arrayOf(
Button("Btn1"),
Button("Btn2"),
Button("Btn3"),
Button("Btn4"),
Button("Btn5")
)
private var pageSize = 3 //Decide this however you want
override val root = tabpane {
setPrefSize(800.0, 600.0)
tabs.removeAll()
tabClosingPolicy = TabPane.TabClosingPolicy.ALL_TABS
tab("My Tab 1") {
hbox {
button("Click Me!") {
setOnAction {
replaceWith<TestView>()
}
}
button("Add Tab 2") {
setOnAction {
tab("tab2 ")
{
select()
button("Close") { setOnAction { close() } }
//The ceiling calc is for rounding up for the size
pagination(ceil(buttonList.size.toDouble() / pageSize).toInt())
{ setPageFactory { pageIndex -> createPage(pageIndex * pageSize, pageSize) } }
style {
arrowsVisible = false
pageInformationVisible = false
}
}
}
}
}
}
}
private fun createPage(startingIndex: Int, pageSize: Int = 1) : Pane {
val pagePane = VBox() //Create and design whatever pane you want
val endingIndex = minOf(startingIndex + pageSize, buttonList.size) //Don't go out of bounds
for(i in startingIndex until endingIndex) { //Assuming starting index is in bounds
pagePane.add(buttonList[i])
}
return pagePane
}
}
Here is a solution for dynamic pages for pagination. Your main problem was that tab panes only hold one child pane/component. The answer is to nest your buttons in a pane. I haven't tested this hard for edge cases or anything, so this is more proof-of-concept. You'll have to account for those if they come up.
Side Notes: As explained above, this is also why your "Close" button isn't appearing. Also, when I was testing your code I noticed the style wasn't implemented unless I clicked a button in the pane. Maybe it would work if you used a stylesheet, created a cssclass, and added that class to your tabpane (Don't forget to import your stylesheet if you choose to do this).
A snippet of my code is as follows:
//button clicked method
#FXML
public void newHeatExchanger2ButtonClicked(ActionEvent event) throws Exception {
//new pane created
Pane pane = new Pane();
//method call everytime the button is clicked
create2DExchanger(pane);
}
//method declaration
private void create2DExchanger(Pane pane) {
EventHandler<MouseEvent> panePressed = (e -> {
if (e.getButton() == MouseButton.SECONDARY){
do stuff
}
if (e.getButton() == MouseButton.PRIMARY){
do stuff
}
});
EventHandler<MouseEvent> paneDragged = (e -> {
if (e.getButton() == MouseButton.PRIMARY){
do stuff
}
});
EventHandler<MouseEvent> paneReleased = (e -> {
if (e.getButton() == MouseButton.PRIMARY){
do stuff;
}
});
EventHandler<MouseEvent> paneMoved = (t -> {
do stuff;
});
EventHandler<MouseEvent> paneClicked = (t -> {
//I need this filter to remove itself right here
t.consume();
pane.removeEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.addEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
});
pane.removeEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.removeEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.removeEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
pane.addEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventFilter(MouseEvent.MOUSE_PRESSED, paneClicked);
}
Initially I set the pane to have only the event filters of mouse_moved, and mouse_pressed. As soon as the mouse is clicked I need the mouse filter for mouse_pressed and mouse_moved to go away and add the eventHandlers as I do in the paneClicked filter llamda. I need the first set of events to be filters because there are children nodes I do not want to receive the event (i.e. an event filter on an arc that is a child of the pane). The second set need to be handlers because the arc event filter needs to consume the event before the pane eventHandlers receive it.
Convenience events like:
pane.setOnMousePressed()
can remove themselves by calling
pane.setOnMousePressed(null);
but I need this initial event filter to remove itself. I will need the functionality of an event removing itself later as well in the code, but if I try to add
pane.removeEventFilter(MouseEvent.MOUSE_PRESSED, paneClicked);
to
EventHandler<MouseEvent> paneClicked = (t -> {
//I need this filter to remove itself right here
t.consume();
pane.removeEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.addEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
});
It will not compile. I have been researching for a couple of days on how to get the functionality of an eventFilter or eventHandler to remove itself but I am coming up short. I have not found anything online or on stackexchange in my Google searches either. I would really appreciate being able to figure this thing out. Thanks.
I believe the problem stems from the fact that you try to access paneClicked before it was fully declared.
You can overcome this using an anonymous class with the this keyword:
EventHandler<MouseEvent> paneClicked = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
event.consume();
pane.removeEventFilter(MouseEvent.MOUSE_PRESSED, this);
pane.removeEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.addEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
}
};
Or by referencing a fully qualified static function:
public class ContainingClass {
...
private static EventHandler<MouseEvent> paneClicked = (t -> {
t.consume();
pane.removeEventFilter(
MouseEvent.MOUSE_PRESSED, ContainingClass.paneClicked
);
});
}
See also:
Why do lambdas in Java 8 disallow forward reference to member variables where anonymous classes don't?
Java8 Lambdas vs Anonymous classes
I have a log in form that i was creating and i want the TextField ActionEvent to be used also in the Button but I don't know what to do. In Swing I've seen it that it can recycle an ActionEvent and use it in other like TextField but i don't know how to do it in JavaFX.
Here is a code for my TextField with an ActionEvent
and I want to apply this also to my Button so I dont have to create another method with just the same function but different Component. Thanks
passField.setOnKeyPressed(new EventHandler<KeyEvent>()
{
#Override
public void handle(KeyEvent e)
{
if(e.getCode().equals(KeyCode.ENTER))
{
if(admin.equals(userField.getText()) && password.equals(passField.getText()))
{
textInfo.setText("WELCOME " + passField.getText());
textInfo.setTextFill(Color.BLACK);
}
else
{
userField.clear();
passField.clear();
textInfo.setText("Incorrect username or password");
textInfo.setTextFill(Color.RED);
}
}
}
});
You will have to find a shared Event that both the Button and the TextField support.
In your example you are attaching a handler for a KeyEvent watching for the ENTER key, which is equivalent to an ActionEvent. luckily the Button supports it too.
Create a shared EventHandler:
final EventHandler<ActionEvent> myHandler = e -> {
if(admin.equals(userField.getText()) && password.equals(passField.getText())) {
textInfo.setText("WELCOME " + passField.getText());
textInfo.setTextFill(Color.BLACK);
}
else {
userField.clear();
passField.clear();
textInfo.setText("Incorrect username or password");
textInfo.setTextFill(Color.RED);
}
}
Which you can now attach twice (or even more often):
button.setOnAction(myHandler);
passField.setOnAction(myHandler);
EDIT
Without lambda expression:
final EventHandler<ActionEvent> myHandler = new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e)
{
if(admin.equals(userField.getText()) && password.equals(passField.getText())) {
textInfo.setText("WELCOME " + passField.getText());
textInfo.setTextFill(Color.BLACK);
}
else {
userField.clear();
passField.clear();
textInfo.setText("Incorrect username or password");
textInfo.setTextFill(Color.RED);
}
}
});
I'm having a hard time by setting back the cursor to position 0 in the first line, after clearing the text from the textarea.
Problem Background
I have a textarea, which listens to keyevents. The textarea only listens to Enter key, if found, either submits the text or clears the text if there is a "\n" in the text area.
I tried all of the following, but none really worked out.
textArea.setText("")
textArea.clear()
textArea.getText().replace("\n", "")
Remove focus from it and put it back again.
Here is a test project that is runnable and demonstrates the problem.
The Main class:
public class Main extends Application {
Stage primaryStage;
AnchorPane pane;
public void start(Stage primaryStage){
this.primaryStage = primaryStage;
initMain();
}
public void initMain(){
try {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(Main.class.getResource("main.fxml"));
pane = loader.load();
Controller controller = loader.getController();
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String args[]){
launch();
}
}
The Controller class:
public class Controller {
#FXML
TextArea textArea;
public void initialize() {
textArea.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent keyEvent) {
if (keyEvent.getCode() == KeyCode.ENTER) {
if (!textArea.getText().equals("")
&& !textArea.getText().contains("\n")) {
handleSubmit();
}
if (textArea.getText().contains("\n")) {
handleAsk();
}
}
}
});
}
/**
* After the user gives in a short input, that has no \n, the user submits by hitting enter.
* This method will be called, and the cursor jumps over to the beginning of the next line.
*/
public void handleSubmit(){
System.out.println("Submitted");
}
/**
* When this method is calls, the cursor is not in the first line.
* This method should move the cursor to the first position in the first line in the completely
* cleared text area.
*/
public void handleAsk(){
System.out.println("Asking and clearing text area.");
textArea.setText("");
}
}
The fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="317.0" prefWidth="371.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="Controller">
<children>
<TextArea fx:id="textArea" layoutX="78.0" layoutY="59.0" prefHeight="114.0" prefWidth="200.0" />
</children>
</AnchorPane>
My problem is, that the cursor won't jump back...
I found a short solution: after I called the desired method (handleAsk()), that should clear the text area when its finished, I call: keyEvent.consume();
This consumes the default effect of ENTER.
So: First, the event handler that you explicitly define does it's job, and then you can decide if you want to have as a "side effect" the default effect of the given key event, if not, you can consume it.
So, either your key handler will be invoked before the text is changed (due to something being typed), or it will be invoked after the text is changed.
You are presumably hoping it will be invoked before, since otherwise
textArea.getText().contains("\n")
would always evaluate to true (since the user just pressed the Enter key).
But in this case, on the second press of Enter, you will clear the text before the text is then modified. So you clear the text, then the new line is added (from the user pressing Enter). Hence the blank line in the text area.
You probably don't want to rely on the order of the events being processed anyway. It turns out that the text is modified on keyTyped events (I think), but that's not documented, and so there's really no guarantee of it. The safer way is to listen to changes in the text, and then to count the number of newlines:
textArea.textProperty().addListener((obs, oldText, newText) -> {
int oldNewlines = countNewlines(oldText); // note, can be more efficient by caching this
int numNewlines = countNewlines(newText);
if (numNewlines == 1 && oldNewlines != 1) {
// submit data
} else if (numNewlines == 2) {
textArea.clear();
}
});
With
private int countNewlines(String text) {
int newlines = 0 ;
while (text.indexOf("\n") >= 0) {
newlines++ ;
text = text.substring(text.indexOf("\n") + 1);
}
return newlines ;
}
(or some other implementation, eg using regex).
I would like to write a method that will print the entire scene graph (including the names of the nodes and the css properties associated with the nodes) to the console. This would enable viewing the hierarchy of the scene graph and all the css attributes associated with each element. I tried to do this (see the scala code below) but my attempt failed.
The scene graph I'm testing this on contains a custom controller that I made following this tutorial. It is not printing any of the nested controls contained in the custom controller. The custom controller appears fine on the stage (and it functions properly) so I know all the required controls are part of the scene graph, but for some reason the example code does not recurse into the custom controller. It does print the name of the custom controller, but not the nested elements inside the controller.
object JavaFXUtils {
def printNodeHierarchy(node: Node): Unit = {
val builder = new StringBuilder()
traverse(0, node, builder)
println(builder)
}
private def traverse(depth: Int, node: Node, builder: StringBuilder) {
val tab = getTab(depth)
builder.append(tab)
startTag(node, builder)
middleTag(depth, node, builder)
node match {
case parent: Parent => builder.append(tab)
case _ =>
}
endTag(node, builder)
}
private def getTab(depth: Int): String = {
val builder = new StringBuilder("\n")
for(i <- 0 until depth) {
builder.append(" ")
}
builder.toString()
}
private def startTag(node: Node, builder: StringBuilder): Unit = {
def styles: String = {
val styleClasses = node.getStyleClass
if(styleClasses.isEmpty) {
""
} else {
val b = new StringBuilder(" styleClass=\"")
for(i <- 0 until styleClasses.size()) {
if(i > 0) {
b.append(" ")
}
b.append(".").append(styleClasses.get(i))
}
b.append("\"")
b.toString()
}
}
def id: String = {
val nodeId = node.getId
if(nodeId == null || nodeId.isEmpty) {
""
} else {
val b = new StringBuilder(" id=\"").append(nodeId).append("\"")
b.toString()
}
}
builder.append(s"<${node.getClass.getSimpleName}$id$styles>")
}
private def middleTag(depth: Int, node: Node, builder: StringBuilder): Unit = {
node match {
case parent: Parent =>
val children: ObservableList[Node] = parent.getChildrenUnmodifiable
for (i <- 0 until children.size()) {
traverse(depth + 1, children.get(i), builder)
}
case _ =>
}
}
private def endTag(node: Node, builder: StringBuilder) {
builder.append(s"</${node.getClass.getSimpleName}>")
}
}
What is the proper way to print the contents of the entire scene graph? The accepted answer can be written in either Java or Scala.
Update
Upon further review, I'm noticing that it does work properly for some of the custom controls, but not all of them. I have 3 custom controls. I'll demonstrate 2 of them. The custom controls are ClientLogo and MenuViewController. The previously listed traversal code properly shows the children of the ClientLogo, but does not show the children of the MenuViewController. (Maybe this is because the MenuViewController is a subclass of TitledPane?)
client_logo.fxml:
<?import javafx.scene.image.ImageView?>
<fx:root type="javafx.scene.layout.StackPane" xmlns:fx="http://javafx.com/fxml" id="clientLogo">
<ImageView fx:id="logo"/>
</fx:root>
ClientLogo.scala
class ClientLogo extends StackPane {
#FXML #BeanProperty var logo: ImageView = _
val logoFxml: URL = classOf[ClientLogo].getResource("/fxml/client_logo.fxml")
val loader: FXMLLoader = new FXMLLoader(logoFxml)
loader.setRoot(this)
loader.setController(this)
loader.load()
logo.setImage(Config.clientLogo)
logo.setPreserveRatio(false)
var logoWidth: Double = .0
def getLogoWidth = logoWidth
def setLogoWidth(logoWidth: Double) {
this.logoWidth = logoWidth
logo.setFitWidth(logoWidth)
}
var logoHeight: Double = .0
def getLogoHeight = logoHeight
def setLogoHeight(logoHeight: Double) {
this.logoHeight = logoHeight
logo.setFitHeight(logoHeight)
}
}
Usage of ClientLogo:
<ClientLogo MigPane.cc="id clientLogo, pos (50px) (-25px)"/>
menu.fxml:
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.shape.Line?>
<?import javafx.scene.control.Label?>
<fx:root type="javafx.scene.control.TitledPane" xmlns:fx="http://javafx.com/fxml" id="menu" collapsible="true" expanded="false">
<GridPane vgap="0" hgap="0">
<children>
<Line fx:id="line" styleClass="menu-line" startX="0" startY="1" endX="150" endY="1" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<Label fx:id="btnSettings" id="btn-settings" styleClass="btn-menu" text="%text.change.password" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
<Label fx:id="btnAdmin" id="btn-admin" styleClass="btn-menu" text="%text.admin" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
<Label fx:id="btnQuickTips" id="btn-quick-tips" styleClass="btn-menu" text="%text.quick.tips" GridPane.rowIndex="3" GridPane.columnIndex="0"/>
<Label fx:id="btnLogout" id="btn-logout" styleClass="btn-menu" text="%text.logout" GridPane.rowIndex="4" GridPane.columnIndex="0"/>
</children>
</GridPane>
</fx:root>
MenuViewController.scala:
class MenuViewController extends TitledPane with ViewController[UserInfo] with LogHelper with Resources {
private var menuWidth: Double = .0
#FXML #BeanProperty var line: Line = _
#FXML #BeanProperty var btnSettings: Label = _
#FXML #BeanProperty var btnAdmin: Label = _
#FXML #BeanProperty var btnQuickTips: Label = _
#FXML #BeanProperty var btnLogout: Label = _
val menuFxml: URL = classOf[MenuViewController].getResource("/fxml/menu.fxml")
val loader: FXMLLoader = new FXMLLoader(menuFxml)
loader.setRoot(this)
loader.setController(this)
loader.setResources(resources)
loader.load()
var userInfo: UserInfo = _
#FXML
private def initialize() {
def handle(message: Any): Unit = {
setExpanded(false)
uiController(message)
}
btnSettings.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(SettingsClicked)))
btnAdmin.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(AdminClicked)))
btnQuickTips.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(QuickTipsClicked)))
btnLogout.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(LogoutClicked)))
}
override def update(model: UserInfo) {
userInfo = model
setText(if (userInfo == null) "Menu" else userInfo.displayName)
}
def getMenuWidth: Double = {
return menuWidth
}
def setMenuWidth(menuWidth: Double) {
this.menuWidth = menuWidth
val spaceToRemove: Double = (menuWidth / 3)
line.setEndX(menuWidth - spaceToRemove)
line.setTranslateX(spaceToRemove / 2)
Array(btnSettings, btnAdmin, btnQuickTips, btnLogout).foreach(btn => {
btn.setMinWidth(menuWidth)
btn.setPrefWidth(menuWidth)
btn.setMaxWidth(menuWidth)
})
}
}
Usage of MenuViewController:
<MenuViewController MigPane.cc="id menu, pos (100% - 250px) (29)" menuWidth="200" fx:id="menu"/>
This is a sample of the output from printing the scene graph to the console:
<BorderPane styleClass=".root">
<StackPane>
<MigPane id="home" styleClass=".top-blue-bar-bottom-blue-curve">
<ClientLogo id="clientLogo">
<ImageView id="logo"></ImageView>
</ClientLogo>
...
<MenuViewController id="menu" styleClass=".titled-pane">
</MenuViewController>
</MigPane>
</StackPane>
</BorderPane>
As you can see, the child elements in the ClientLogo custom control are properly traversed, but the elements inside menu.fxml are missing. I would expect to see the substructure that is added to a TitledPane and the GridPane with its nested substructure listed in menu.fxml.
Suggested Tool
ScenicView is a 3rd party tool for introspecting on the SceneGraph. It is highly recommended that you use ScenicView rather than developing your own debugging tools to dump the Scene Graph.
Dump Utility
Here is a very simple little routine to dump the scene graph to System.out, invoke it via DebugUtil.dump(stage.getScene().getRoot()):
import javafx.scene.Node;
import javafx.scene.Parent;
public class DebugUtil {
/** Debugging routine to dump the scene graph. */
public static void dump(Node n) {
dump(n, 0);
}
private static void dump(Node n, int depth) {
for (int i = 0; i < depth; i++) System.out.print(" ");
System.out.println(n);
if (n instanceof Parent) {
for (Node c : ((Parent) n).getChildrenUnmodifiable()) {
dump(c, depth + 1);
}
}
}
}
The simple routine above will usually dump all of the style class info because the default toString() on node outputs styleclass data.
An additional enhancement would be to check if the node being printed is also an instance of Labeled or Text, then also print the text string associated with the node in those cases.
Caveat
For some control types, some of the nodes which are created for the control are instantiated by the control's skin which can be defined in CSS and the skin sometimes creates its child nodes lazily.
What you need to do to see those nodes is ensure a css layout pass has been run on the scene graph by running your dump after one of the following events:
stage.show() has been called for an initial scene.
After a pulse occurs for a modified scene.
use AnimationTimer for this to count a pulse.
After you have manually forced a layout pass.
taking a synchronous snapshot of the scene will force a layout pass, though I think there may be some other APIs that I can't remember the name of which allow you to force the layout pass without a snapshot.
Check that you have done one of these things before you run your dump.
Example of using an AnimationTimer
// Modify scene.
// . . .
// Start a timer which delays the scene dump call.
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
// Take action on receiving a pulse.
dump(stage.getScene().getRoot());
// Stop monitoring pulses.
// You can stop immediately like this sample.
stop();
// OR you could count a specified number pulses before stopping.
}
};
timer.start();
Based on #jewelsea's excellent answer , heres a version in Jython. It traverses a bit deeper by using a special case handler for controls that dont respond well to the findchildren method. It should be straight forward enough to translate into generic java, just use the python code as pseudocode!
from javafx.scene import Parent
from javafx.scene import control
class SceneGraph(object):
def parse(self,n,depth=0):
print " "*depth,n
if isinstance(n,control.Tab):
for i in n.getContent().getChildrenUnmodifiable():
self.parse(i,depth+1)
elif isinstance(n,control.TabPane):
for i in n.getTabs():
self.parse(i,depth+1)
elif isinstance(n,Parent):
for i in n.getChildrenUnmodifiable():
self.parse(i,depth+1)
elif isinstance(n,control.Accordion):
for i in n.getPanes():
self.parse(i,depth+1)
Special handlers might be needed for other controls, and the code could perhaps be improved by a closer examination of the class heirachy.
I'm not entirely sure how best to handle third party control packs, at this stage.