8 Replies Latest reply: Dec 18, 2012 8:58 AM by Kishori Sharan RSS

    Issue with coordinate space conversion when the stage is resized

    Kishori Sharan
      Hello,
      I am trying to place a circle at the top left corner of the node that has the focus. The following code works, if I change the focus using mouse or the tab key. If I resize the stage, by making it smaller, the circle is not positioned correctly. The coordinate conversions return the values as if the stage was not resized. That is, adding the change listeners to the width and height properties of the stage does not make any difference.

      Can someone point me in the right direction as to what I am missing?

      Thanks
      Kishori
      // CoordinateConversion.java
      package test;
      
      import javafx.application.Application;
      
      import javafx.beans.value.ChangeListener;
      import javafx.beans.value.ObservableValue;
      
      import javafx.geometry.Point2D;
      
      import javafx.scene.Node;
      import javafx.scene.Scene;
      import javafx.scene.control.Label;
      import javafx.scene.control.TextField;
      import javafx.scene.layout.HBox;
      import javafx.scene.layout.VBox;
      import javafx.scene.paint.Color;
      import javafx.scene.shape.Circle;
      
      import javafx.stage.Stage;
      
      
      public class CoordinateConversion extends Application {
           // An instance variable to store the reference of the circle
           private Circle marker;
      
           public static void main(String[] args) {
                Application.launch(args);
           }
      
           public void start(Stage stage) {
                TextField fName = new TextField();
                TextField lName = new TextField();
                TextField salary = new TextField();
      
                // The Circle node is unmanaged
                marker = new Circle(5);
                marker.setManaged(false);
                marker.setFill(Color.RED);
                marker.setMouseTransparent(true);
      
                HBox hb1 = new HBox();
                HBox hb2 = new HBox();
                HBox hb3 = new HBox();
                hb1.getChildren().addAll(new Label("First Name:"), fName);
                hb2.getChildren().addAll(new Label("Last Name:"), lName);
                hb3.getChildren().addAll(new Label("Salary:"), salary);
      
                VBox root = new VBox();
                root.getChildren().addAll(hb1, hb2, hb3, marker);
      
                final Scene scene = new Scene(root);
      
                // Add a focus change listener to the scene
                scene.focusOwnerProperty().addListener(new ChangeListener() {
                     @Override
                     public void changed(ObservableValue prop, Object oldNode, Object newNode) {
                          layoutMarker(scene.getFocusOwner());
                     }
                });
      
                stage.widthProperty().addListener(new ChangeListener() {
                     @Override
                     public void changed(ObservableValue observableValue, Object t, Object t1) {
                          layoutMarker(scene.getFocusOwner());
                     }
                });
      
                stage.heightProperty().addListener(new ChangeListener() {
                     @Override
                     public void changed(ObservableValue observableValue, Object t, Object t1) {
                          layoutMarker(scene.getFocusOwner());
                     }
                });
      
      
                stage.setScene(scene);
                stage.setTitle("Coordinate Space Transformation");
                stage.show();
           }
      
           public void layoutMarker(Node newNode) {
                if (newNode == null) {
                     return;
                }
      
                double nodeMinX = newNode.getLayoutBounds().getMinX();
                double nodeMinY = newNode.getLayoutBounds().getMinY();
                Point2D nodeInScene = newNode.localToScene(nodeMinX, nodeMinY);
                Point2D nodeInMarkerLocal = marker.sceneToLocal(nodeInScene);
                Point2D nodeInmarkerParent = marker.localToParent(nodeInMarkerLocal);
                marker.relocate(nodeInmarkerParent.getX() + marker.getLayoutBounds().getMinX(),
                                    nodeInmarkerParent.getY() + marker.getLayoutBounds().getMinY());
           }
      }
      Edited by: Kishori Sharan on Dec 13, 2012 7:49 AM
        • 1. Re: Issue with coordinate space conversion when the stage is resized
          shakir.gusaroff
          I think it is a label problem. It works with a text.
          • 2. Re: Issue with coordinate space conversion when the stage is resized
            James_D
            I was going to reply to say that this code worked just fine for me. Then I experimented to make the labels take up additional space when the window was resized to test it a bit more. It still seemed to work fine until I dragged the mouse very quickly, and then I saw a lag, with the marker not being positioned in the correct place.

            So, what I think is happening, is that your handler for the resizing of the window is getting invoked before the layout of the children is being performed. (So if you move the mouse nice and slowly, you'll maybe be off by a pixel or two; the layout will have been updated the last time the mouse being dragged was detected. If you move the mouse quickly, the window will have been resized by many pixels between invocations of your listener. The effect is probably quite implementation-dependent.)

            Incidentally, there's one other thing you should be aware of with your code. Repositioning the marker when the focus changes or when the stage is resized is fine as long as your layout is static. But if the layout were to change for some other reason (e.g., by changing the text of your labels), the marker will end up in the wrong place.

            So what you really need, is for the marker to be repositioned when the bounds of the focused control changes in the overall scene. Unfortunately, there's no boundsInScene property, so the only way I can see to do this is to listen to changes in the boundsInParent property for the focused node and all its ancestors. This feels like a bit of a hack, but here you go:
            import javafx.application.Application;
            import javafx.application.Platform;
             
            import javafx.beans.value.ChangeListener;
            import javafx.beans.value.ObservableValue;
             
            import javafx.geometry.Bounds;
            import javafx.geometry.Point2D;
             
            import javafx.scene.Node;
            import javafx.scene.Scene;
            import javafx.scene.control.Label;
            import javafx.scene.control.TextField;
            import javafx.scene.layout.HBox;
            import javafx.scene.layout.Priority;
            import javafx.scene.layout.VBox;
            import javafx.scene.paint.Color;
            import javafx.scene.shape.Circle;
             
            import javafx.stage.Stage;
             
             
            public class CoordinateConversion extends Application {
              // An instance variable to store the reference of the circle
              private Circle marker;
             
              public static void main(String[] args) {
                Application.launch(args);
              }
             
              @Override
              public void start(Stage stage) {
                TextField fName = new TextField();
                TextField lName = new TextField();
                TextField salary = new TextField();
             
                // The Circle node is unmanaged
                marker = new Circle(5);
                marker.setManaged(false);
                marker.setFill(Color.RED);
                marker.setMouseTransparent(true);
             
                HBox hb1 = new HBox();
                HBox hb2 = new HBox();
                HBox hb3 = new HBox();
                Label fnLabel = new Label("First Name:");
                fnLabel.setMaxWidth(Double.POSITIVE_INFINITY);
                HBox.setHgrow(fnLabel, Priority.ALWAYS);
                hb1.getChildren().addAll(fnLabel, fName);
                Label lnLabel = new Label("Last Name:");
                lnLabel.setMaxWidth(Double.POSITIVE_INFINITY);
                HBox.setHgrow(lnLabel, Priority.ALWAYS);
                hb2.getChildren().addAll(lnLabel, lName);
                Label selLabel = new Label("Salary:");
                selLabel.setMaxWidth(Double.POSITIVE_INFINITY);
                HBox.setHgrow(selLabel, Priority.ALWAYS);
                hb3.getChildren().addAll(selLabel, salary);
             
                VBox root = new VBox();
                root.getChildren().addAll(hb1, hb2, hb3, marker);
             
                final Scene scene = new Scene(root);
                
                // listener for changes in bounds that relocates the marker
                final ChangeListener<Bounds> relocateListener = new ChangeListener<Bounds>() {
                  @Override
                  public void changed(ObservableValue<? extends Bounds> observable,
                      Bounds oldValue, Bounds newValue) {
                    layoutMarker(scene.getFocusOwner());
                  }
                };
             
                // Add a focus change listener to the scene
                scene.focusOwnerProperty().addListener(new ChangeListener<Node>() {
                  @Override
                  public void changed(ObservableValue<? extends Node> node, Node oldNode, Node newNode) {
                    layoutMarker(newNode);
                    removeListenerFromNodeAndParents(relocateListener, oldNode);
                    addListenerToNodeAndParents(relocateListener, newNode);
                  }
                });
            
             
                stage.setScene(scene);
                stage.setTitle("Coordinate Space Transformation");
                stage.show();
              }
              
              private void addListenerToNodeAndParents(ChangeListener<Bounds> listener, Node node) {
                for (Node n = node ; n!=null ; n=n.getParent()) {
                  n.boundsInParentProperty().addListener(listener);
                }
              }
              
              private void removeListenerFromNodeAndParents(ChangeListener<Bounds> listener, Node node) {
                for (Node n = node; n!=null ; n=n.getParent()) {
                  n.boundsInParentProperty().removeListener(listener);
                }
              }
             
              public void layoutMarker(final Node newNode) {
                if (newNode == null) {
                  return;
                }
                
                double nodeMinX = newNode.getLayoutBounds().getMinX();
                double nodeMinY = newNode.getLayoutBounds().getMinY();
                Point2D nodeInScene = newNode.localToScene(nodeMinX, nodeMinY);
                Point2D nodeInMarkerLocal = marker.sceneToLocal(nodeInScene);
                Point2D nodeInmarkerParent = marker.localToParent(nodeInMarkerLocal);
                marker.relocate(nodeInmarkerParent.getX() + marker.getLayoutBounds().getMinX(),
                        nodeInmarkerParent.getY() + marker.getLayoutBounds().getMinY());        
             
              }
            }
            Edited by: James_D on Dec 13, 2012 11:33 AM (Removed unnecessary Platform.runLater(...) invocation left over from experimenting.)
            • 3. Re: Issue with coordinate space conversion when the stage is resized
              Kishori Sharan
              Thank you James_D for your response.

              Your solution works. However, in my opinion, it sounds overkill to achieve what seems to be a simple thing. Because it adds a bounds change listener to the node super-tree hierarchy, the relayout code executes as many times as the number of elements that get resized for one resize event in the focused node super-tree. You can confirm it by printing a message from the layoutMarker() method. When I resized the stage, the relayout code got fired seven times!

              Here is the optimized version based on your concept. I added a change listener to the localToSceneTransform property (introduced in JavaFX 2.2) to the focused node only. Because we need to care only about the change in layout for the focused node, the relayout code executes only once for one resize event in the stage, if that resize event changes the bounds of the focused node relative to the scene. This solution works, if I focus only on the focused node. How about doing a processing (that uses bounds of several nodes) if the stage is resized? We will have to revert to your solution and add a bounds change listener to all nodes in the scene.


              My question remains:
              What is the recommened way in JavaFX to capture the stage resize event in which the changed bounds of nodes are available correctly?

              Thanks again for your time and the response.

              Kishori
              package test;
              
              import javafx.application.Application;
              
              import javafx.beans.value.ChangeListener;
              import javafx.beans.value.ObservableValue;
              
              import javafx.geometry.Point2D;
              
              import javafx.scene.Node;
              import javafx.scene.Scene;
              import javafx.scene.control.Label;
              import javafx.scene.control.TextField;
              import javafx.scene.layout.HBox;
              import javafx.scene.layout.VBox;
              import javafx.scene.paint.Color;
              import javafx.scene.shape.Circle;
              import javafx.scene.transform.Transform;
              
              import javafx.stage.Stage;
              
              
              public class Test3 extends Application {
                   // An instance variable to store the reference of the circle
                   private Circle marker;
              
                   public static void main(String[] args) {
                        Application.launch(args);
                   }
              
                   @Override
                   public void start(Stage stage) {
                        TextField fName = new TextField();
                        TextField lName = new TextField();
                        TextField salary = new TextField();
              
                        // The Circle node is unmanaged
                        marker = new Circle(5);
                        marker.setManaged(false);
                        marker.setFill(Color.RED);
                        marker.setMouseTransparent(true);
              
                        HBox hb1 = new HBox();
                        HBox hb2 = new HBox();
                        HBox hb3 = new HBox();
                        Label fnLabel = new Label("First Name:");
                        hb1.getChildren().addAll(fnLabel, fName);
                        Label lnLabel = new Label("Last Name:");
              
                        hb2.getChildren().addAll(lnLabel, lName);
                        Label selLabel = new Label("Salary:");
              
                        hb3.getChildren().addAll(selLabel, salary);
              
                        VBox root = new VBox();
                        root.getChildren().addAll(hb1, hb2, hb3, marker);
              
                        final Scene scene = new Scene(root);
              
                        // listener for changes in bounds that relocates the marker
                        final ChangeListener<Transform> relocateListener = new ChangeListener<Transform>() {
                             @Override
                             public void changed(ObservableValue<? extends Transform> observable, Transform oldValue, Transform newValue) {
                                  layoutMarker(scene.getFocusOwner());
                             }
                        };
              
                        // Add a focus change listener to the scene
                        scene.focusOwnerProperty().addListener(new ChangeListener<Node>() {
                             @Override
                             public void changed(ObservableValue<? extends Node> node, Node oldNode, Node newNode) {
                                  layoutMarker(newNode);
                                  removeListenerFromNodeAndParents(relocateListener, oldNode);
                                  addListenerToNodeAndParents(relocateListener, newNode);
                             }
                        });
              
                        stage.setScene(scene);
                        stage.setTitle("Coordinate Space Transformation");
                        stage.show();
                   }
              
                   private void addListenerToNodeAndParents(ChangeListener<Transform> listener, Node node) {
                        node.localToSceneTransformProperty().addListener(listener);
                   }
              
                   private void removeListenerFromNodeAndParents(ChangeListener<Transform> listener, Node node) {
                        if (node != null) {
                             node.localToSceneTransformProperty().removeListener(listener);
                        }
                   }
              
                   public void layoutMarker(final Node newNode) {
                        if (newNode == null) {
                             return;
                        }
              
                        System.out.println("Laying out marker..: \n-------------------");
              
                        double nodeMinX = newNode.getLayoutBounds().getMinX();
                        double nodeMinY = newNode.getLayoutBounds().getMinY();
                        Point2D nodeInScene = newNode.localToScene(nodeMinX, nodeMinY);
                        Point2D nodeInMarkerLocal = marker.sceneToLocal(nodeInScene);
                        Point2D nodeInmarkerParent = marker.localToParent(nodeInMarkerLocal);
                        marker.relocate(nodeInmarkerParent.getX() + marker.getLayoutBounds().getMinX(),
                                            nodeInmarkerParent.getY() + marker.getLayoutBounds().getMinY());
              
                   }
              }
              • 4. Re: Issue with coordinate space conversion when the stage is resized
                James_D
                Kishori Sharan wrote:
                Thank you James_D for your response.

                Your solution works. However, in my opinion, it sounds overkill to achieve what seems to be a simple thing. Because it adds a bounds change listener to the node super-tree hierarchy, the relayout code executes as many times as the number of elements that get resized for one resize event in the focused node super-tree. You can confirm it by printing a message from the layoutMarker() method. When I resized the stage, the relayout code got fired seven times!

                Here is the optimized version based on your concept. I added a change listener to the localToSceneTransform property (introduced in JavaFX 2.2) to the focused node only. Because we need to care only about the change in layout for the focused node, the relayout code executes only once for one resize event in the stage, if that resize event changes the bounds of the focused node relative to the scene. This solution works, if I focus only on the focused node. How about doing a processing (that uses bounds of several nodes) if the stage is resized? We will have to revert to your solution and add a bounds change listener to all nodes in the scene.
                Ah, that's nice. I wasn't aware of the localToSceneTransform property. That's a better solution.

                The performance gain may not be as much as you think, though. On the one hand, listening for changes in the localToSceneTransform property does a little more work than you think. That property registers invalidation listeners with all the parents of the node, up to the Scene. So this, like my solution, scales with the depth of the scene graph. On the other hand, the solution I had probably does a little less work than you imagine. Note that we're not observing every node in the scene, just the ones from the focused node up the scene graph to the root node. While the layoutMarker() method is called once for each node from the "node of interest" all the way up to the scene, only one of those invocations actually changes the layout bounds of the marker. (You can verify this by displaying the layoutX and layoutY before and after each marker.relocate(...) call.) It seems reasonable to assume a call to relocate(...) passing in the current layoutX and layoutY acts as a no-op; though you could certainly check for a "real" change and not invoke marker.locate(...) if necessary. So I'd argue that my solution just involves reading the values of a few variables, rather than actually changing the layout of the scene graph more than is really necessary. The localToSceneTransform is a better solution though; it's cleaner code (far fewer listener registrations to track) and performs at least slightly better.

                >
                >
                My question remains:
                What is the recommened way in JavaFX to capture the stage resize event in which the changed bounds of nodes are available correctly?
                I still wonder if this is really the correct question, though. Your use case here is really intended to move your marker when the location of the control in the scene changes, not when the size of the window changes. Suppose you had an answer to your question as stated; and suppose you had a status label in your scene graph whose value changed, say, as a background thread was executing some task. When that label changed its text, the layout would change (so your marker would need to be moved), even though the window hadn't changed size. So simply listening for changes in the stage resize wouldn't be enough to satisfy this use case in general. The minimal solution has to involve a listener which responds to a change in location (relative to the scene) of any node in which you are interested: I think you have solved this problem.
                • 5. Re: Issue with coordinate space conversion when the stage is resized
                  Kishori Sharan
                  Thanks you James_D once again for your response. I am in complete agreement with you for the solution for the use case in this example. However, to get to the real question that I "intended" to ask (I know you can't read my mind.), let me give you some background.

                  I was working on an example to explain how to transform a point in one coordinate space to another using localTo...(), sceneToXXX(), etc. methods in the Node class. This is the reason that I have placed each pair of the (Label, TextField) in a separate HBox, so they are under different parents from the marker circle. In my first attempt, I added a focus change listener to the scene's focusOwner property. I was done with my example as this served the purpose of explaining the coordinate space transformations. Then, I resized the stage and found that marker was not placed correctly. I thought adding change listeners to the width and height properties of the stage should do. Unfortunately, it did not. This led me to start this thread.

                  Now, let me rephrase my question, as you have pointed out another thing, which is true, that the focused node may change its bound without stage getting resized:

                  Is there a way to hook a listener that is notified, when part or whole scene graph is re-laid out?



                  Thanks
                  Kishori
                  • 6. Re: Issue with coordinate space conversion when the stage is resized
                    James_D
                    Kishori Sharan wrote:

                    Now, let me rephrase my question, as you have pointed out another thing, which is true, that the focused node may change its bound without stage getting resized:

                    Is there a way to hook a listener that is notified, when part or whole scene graph is re-laid out?
                    As far as I can tell, no. I don't know much about how this is implemented, but I'd actually imagine that if the scene graph is modified only from a particular node down, then the nodes above that would not know about the changes. Under those circumstances, it's hard to see how there would be a property to which you could attach such a listener. But there may of course be something out there about which I'm unaware.
                    • 7. Re: Issue with coordinate space conversion when the stage is resized
                      shakir.gusaroff
                      Hi Kishori. You need to add a listener to needsLayoutProperty:
                          hb1.needsLayoutProperty().addListener(new ChangeListener() {
                                     @Override
                                     public void changed(ObservableValue prop, Object oldNode, Object newNode) {
                                          layoutMarker(scene.getFocusOwner());
                                     }
                                });
                                      
                      The following works for me:
                      import javafx.application.Application;
                       
                      import javafx.beans.value.ChangeListener;
                      import javafx.beans.value.ObservableValue;
                       
                      import javafx.geometry.Point2D;
                       
                      import javafx.scene.Node;
                      import javafx.scene.Scene;
                      import javafx.scene.control.Label;
                      import javafx.scene.control.TextField;
                      import javafx.scene.layout.HBox;
                      import javafx.scene.layout.VBox;
                      import javafx.scene.paint.Color;
                      import javafx.scene.shape.Circle;
                       
                      import javafx.stage.Stage;
                       
                       
                      public class CoordinateConversion extends Application {
                           // An instance variable to store the reference of the circle
                           private Circle marker;
                       
                           public static void main(String[] args) {
                                Application.launch(args);
                           }
                       
                           public void start(Stage stage) {
                                TextField fName = new TextField();
                                TextField lName = new TextField();
                                TextField salary = new TextField();
                       
                                // The Circle node is unmanaged
                                marker = new Circle(5);
                                marker.setManaged(false);
                                marker.setFill(Color.RED);
                                marker.setMouseTransparent(true);
                       
                                HBox hb1 = new HBox();
                                HBox hb2 = new HBox();
                                HBox hb3 = new HBox();
                                hb1.getChildren().addAll(new Label("First Name:"), fName);
                                hb2.getChildren().addAll(new Label("Last Name:"), lName);
                                hb3.getChildren().addAll(new Label("Salary:"), salary);
                       
                                VBox root = new VBox();
                                root.getChildren().addAll(hb1, hb2, hb3, marker);
                       
                                final Scene scene = new Scene(root);
                       
                                // Add a focus change listener to the scene
                                scene.focusOwnerProperty().addListener(new ChangeListener() {
                                     @Override
                                     public void changed(ObservableValue prop, Object oldNode, Object newNode) {
                                          layoutMarker(scene.getFocusOwner());
                                     }
                                });
                       
                                      
                                      
                                      hb1.needsLayoutProperty().addListener(new ChangeListener() {
                                     @Override
                                     public void changed(ObservableValue prop, Object oldNode, Object newNode) {
                                          layoutMarker(scene.getFocusOwner());
                                     }
                                });
                                      
                                      
                                      
                                      hb2.needsLayoutProperty().addListener(new ChangeListener() {
                                     @Override
                                     public void changed(ObservableValue prop, Object oldNode, Object newNode) {
                                          layoutMarker(scene.getFocusOwner());
                                     }
                                });
                                      
                                      hb3.needsLayoutProperty().addListener(new ChangeListener() {
                                     @Override
                                     public void changed(ObservableValue prop, Object oldNode, Object newNode) {
                                          layoutMarker(scene.getFocusOwner());
                                     }
                                });
                                      
                                      
                                      
                                      
                                      
                                      
                                      
                                      
                                      
                                      
                                      
                                      
                                stage.widthProperty().addListener(new ChangeListener() {
                                     @Override
                                     public void changed(ObservableValue observableValue, Object t, Object t1) {
                                          layoutMarker(scene.getFocusOwner());
                                     }
                                });
                       
                                stage.heightProperty().addListener(new ChangeListener() {
                                     @Override
                                     public void changed(ObservableValue observableValue, Object t, Object t1) {
                                          layoutMarker(scene.getFocusOwner());
                                     }
                                });
                       
                       
                                stage.setScene(scene);
                                stage.setTitle("Coordinate Space Transformation");
                                stage.show();
                           }
                       
                           public void layoutMarker(Node newNode) {
                                if (newNode == null) {
                                     return;
                                }
                       
                                double nodeMinX = newNode.getLayoutBounds().getMinX();
                                double nodeMinY = newNode.getLayoutBounds().getMinY();
                                Point2D nodeInScene = newNode.localToScene(nodeMinX, nodeMinY);
                                Point2D nodeInMarkerLocal = marker.sceneToLocal(nodeInScene);
                                Point2D nodeInmarkerParent = marker.localToParent(nodeInMarkerLocal);
                                marker.relocate(nodeInmarkerParent.getX() + marker.getLayoutBounds().getMinX(),
                                                    nodeInmarkerParent.getY() + marker.getLayoutBounds().getMinY());
                           }
                      }
                      • 8. Re: Issue with coordinate space conversion when the stage is resized
                        Kishori Sharan
                        Thank you Shakir for your response.

                        This is not what I was looking for. The needsLayout property just tells you if a container needs a layout or not. If you get a false in its property changed event that does not guarantee that relayout has already been performed.

                        I am looking for an event to tell me that the scene has been refreshed. I searched this forum one more time and found that few similar questions were asked before and the answer is: No we do not have such an event, at least not as of JavaFX 2.2. The issue is because the scene refresh is performed in another thread. Look at the com.sun.javafx.tk.Toolkit class and its addSceneTkPulseListener() method. This is what we need. However, it is not part of JavaFX public API. Maybe in JavaFX 8.0, we will see something like this.

                        Thanks
                        Kishori