6 Replies Latest reply: May 6, 2013 9:36 PM by shakir.gusaroff RSS

    How to get caret screen coordinates in TextArea?

    1003746
      Hello,

      I am using a TextArea and, when writing some words, I want to display a popup with the suggestions when the user presses CTRL+SPACE. However, I did not find anything that could give me the screen coordinates of the caret.

      So, how can I find out the coordinates of the caret ( I plan to use it in a key press event handler ), or if it's not possible at the moment, what workaround do I have for my scenario? (please note that I want to show the popup at the caret position, and not anywhere on the screen as it would be strange to show it at the center of the TextArea for example).

      Regards,
      subzero

      Edited by: subzero on May 5, 2013 3:19 PM

      Edited by: subzero on May 5, 2013 3:19 PM
        • 1. Re: How to get caret screen coordinates in TextArea?
          James_D
          I can't see a reasonable way to do this. You might want to put a feature request into JIRA.

          I have a totally unreasonable way, though. The caret is represented as a Path (at least, it is in the current versions of JavaFX I'm using; 2.2.7 (JDK 1.7.0_21) and 8.0.0 (JDK 1.8.0 ea b87)); and there appears to be no other Path in the scene graph below a TextArea. So you can just search through the scene graph, starting at the text area, until you find a Path, and then get it's coordinates relative to the screen. This really is a hack; it's highly vulnerable to changes in the implementation of how a TextArea is rendered. The carat has no style class or id set, so using textArea.lookup(...) doesn't seem to help.

          Maybe someone can see a better way?
          import javafx.application.Application;
          import javafx.event.EventHandler;
          import javafx.geometry.Bounds;
          import javafx.geometry.Point2D;
          import javafx.scene.Node;
          import javafx.scene.Parent;
          import javafx.scene.Scene;
          import javafx.scene.control.Label;
          import javafx.scene.control.TextArea;
          import javafx.scene.input.KeyCode;
          import javafx.scene.input.KeyEvent;
          import javafx.scene.layout.BorderPane;
          import javafx.scene.layout.VBox;
          import javafx.scene.shape.Path;
          import javafx.stage.Popup;
          import javafx.stage.Stage;
          import javafx.stage.Window;
          
          public class CaretCoordinatesTest extends Application {
          
            @Override
            public void start(Stage primaryStage) {
              final TextArea textArea = new TextArea();
              final BorderPane root = new BorderPane();
              root.setCenter(textArea);
          
              final Popup popup = new Popup();
              popup.setAutoHide(true);
              VBox suggestionBox = new VBox();
              suggestionBox.setStyle("-fx-border-color: -fx-accent;");
              popup.getContent().add(suggestionBox);
              suggestionBox.getChildren().add(new Label("Here are some suggestions:"));
              for (int i = 1; i <= 10; i++) {
                suggestionBox.getChildren().add(new Label("Suggestion " + i));
              }
          
              textArea.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
                @Override
                public void handle(KeyEvent event) {
                  if (event.getCode() == KeyCode.SPACE && event.isControlDown()) {
                    Path caret = findCaret(textArea);
                    Point2D screenLoc = findScreenLocation(caret);
                    popup.show(textArea, screenLoc.getX(), screenLoc.getY() + 20);
                  }
                }
              });
          
              textArea.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
                @Override
                public void handle(KeyEvent event) {
                  if (popup.isShowing()) {
                    popup.hide();
                  }
                }
              });
          
              primaryStage.setScene(new Scene(root, 600, 400));
              primaryStage.show();
            }
          
            private Path findCaret(Parent parent) {
              // Warning: this is an ENORMOUS HACK
              for (Node n : parent.getChildrenUnmodifiable()) {
                if (n instanceof Path) {
                  return (Path) n;
                } else if (n instanceof Parent) {
                  Path p = findCaret((Parent) n);
                  if (p != null) {
                    return p;
                  }
                }
              }
              return null;
            }
          
            private Point2D findScreenLocation(Node node) {
              double x = 0;
              double y = 0;
              for (Node n = node; n != null; n=n.getParent()) {
                Bounds parentBounds = n.getBoundsInParent();
                x += parentBounds.getMinX();
                y += parentBounds.getMinY();
              }
              Scene scene = node.getScene();
              x += scene.getX();
              y += scene.getY();
              Window window = scene.getWindow();
              x += window.getX();
              y += window.getY();
              Point2D screenLoc = new Point2D(x, y);
              return screenLoc;
            }
          
            public static void main(String[] args) {
              launch(args);
            }
          }
          Edited by: James_D on May 5, 2013 6:04 PM
          • 2. Re: How to get caret screen coordinates in TextArea?
            1003746
            Hi James,

            Well, yes, it's a hack but I must say it's an effective one. I appreciate the sample code, as it's really helpful. One question though,where did you read that the caret is a Path?

            Also I've opened a JIRA to add this functionality: https://javafx-jira.kenai.com/browse/RT-30215

            Again, thank you very much James :)

            Regards,
            subzero
            • 3. Re: How to get caret screen coordinates in TextArea?
              James_D
              subzero wrote:
              One question though,where did you read that the caret is a Path?
              I looked at the source code ("Use the source, Luke"). Specifically [url http://hg.openjdk.java.net/openjfx/8/master/rt/file/tip/javafx-ui-controls/src/javafx/scene/control/TextArea.java]TextArea, [url http://hg.openjdk.java.net/openjfx/8/master/rt/file/tip/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/TextAreaSkin.java]TextAreaSkin, and [url http://hg.openjdk.java.net/openjfx/8/master/rt/file/tip/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/TextInputControlSkin.java]TextInputControlSkin.

              Using the source code in this manner (i.e. to figure out implementation and then write code that specifically relies on that implementation) is generally a really bad idea. If the FX team decide to implement the caret completely differently, your code will break. You'll notice the class where the caret is defined is not actually part of the public API, so it would be completely fair game for them to do this. But like I said, I can't see another way; I'm sort of hoping someone else will come up with a better solution.
              • 4. Re: How to get caret screen coordinates in TextArea?
                1003746
                I understand, guess I should use the sources more, thanks for the advice :)

                Regards,
                subzero
                • 5. Re: How to get caret screen coordinates in TextArea?
                  gimbal2
                  Note that this is power user stuff going on here. You're really good at what you do when you can dig down into the source and figure out what is not publicly documented, but you then still have to think twice before you actually apply what you figure out or in stead adapt your design to the limitations of the technology. In this case I'd find another design, if it were up to me. Otherwise I predict a future post which goes something along the line of "I used to be able to do this but this doesn't work anymore. Any other ways to do the same?"

                  Your requirement seems a very reasonable one that would be beneficial to have in the JavaFX API itself though, I'd certainly log a feature request!
                  • 6. Re: How to get caret screen coordinates in TextArea?
                    shakir.gusaroff
                    Here is another workaround. It can be done by adding a listener to the caretPositionProperty.
                    Below is a modified version of James’s code, tested only on Windows.
                    import javafx.application.Application;
                    import javafx.beans.property.DoubleProperty;
                    import javafx.beans.property.SimpleDoubleProperty;
                    import javafx.beans.value.ChangeListener;
                    import javafx.beans.value.ObservableValue;
                    import javafx.event.EventHandler;
                    import javafx.geometry.Bounds;
                    import javafx.geometry.Point2D;
                    import javafx.scene.Node;
                    import javafx.scene.Parent;
                    import javafx.scene.Scene;
                    import javafx.scene.control.Label;
                    import javafx.scene.control.TextArea;
                    import javafx.scene.input.KeyCode;
                    import javafx.scene.input.KeyEvent;
                    import javafx.scene.layout.BorderPane;
                    import javafx.scene.layout.VBox;
                    import javafx.scene.text.Text;
                    import javafx.stage.Popup;
                    import javafx.stage.Stage;
                    import javafx.stage.Window;
                    
                    public class CaretCoordinatesTest extends Application {
                    
                      
                        DoubleProperty screenLocY = new SimpleDoubleProperty(0);
                        DoubleProperty screenLocX = new SimpleDoubleProperty(0);
                    
                        @Override
                        public void start(final Stage primaryStage) {
                    
                            final TextArea textArea = new TextArea();
                            final BorderPane root = new BorderPane();
                            root.setCenter(textArea);
                    
                            final Popup popup = new Popup();
                            popup.setAutoHide(true);
                            VBox suggestionBox = new VBox();
                            suggestionBox.setStyle("-fx-border-color: -fx-accent;");
                            popup.getContent().add(suggestionBox);
                            suggestionBox.getChildren().add(new Label("Here are some suggestions:"));
                            for (int i = 1; i <= 10; i++) {
                                suggestionBox.getChildren().add(new Label("Suggestion " + i));
                            }
                    
                            textArea.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
                                @Override
                                public void handle(KeyEvent event) {
                                    if (event.getCode() == KeyCode.SPACE && event.isControlDown()) {
                                        popup.show(textArea, screenLocX.getValue(),
                                                screenLocY.getValue() + 20);
                                    }
                                }
                            });
                    
                            textArea.caretPositionProperty().addListener(new ChangeListener<Number>() {
                                public void changed(ObservableValue<? extends Number> observable, Number oldValue, final Number newValue) {
                    
                                    if (newValue != null && newValue.intValue() >= 0) {
                                        Text ty = new Text(textArea.getText(0, newValue.intValue()));
                                        int counter = getLastNewLineCharBeforeCaret(ty.getText(), newValue.intValue());
                    
                                        Text tx = new Text(textArea.getText(counter, newValue.intValue()));
                                        double h = (ty.getLayoutBounds().getHeight() > textArea.getHeight()) ? textArea.getHeight() : ty.getLayoutBounds().getHeight();
                                        screenLocY.set(primaryStage.getY() + textArea.getLayoutY() + h + 20);
                                        screenLocX.set(primaryStage.getX() + textArea.getLayoutX() + tx.getLayoutBounds().getWidth() + 20);
                    
                                    }
                                }
                            });
                    
                            textArea.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
                                @Override
                                public void handle(KeyEvent event) {
                                    if (popup.isShowing()) {
                                        popup.hide();
                                    }
                                }
                            });
                    
                            primaryStage.setScene(new Scene(root, 600, 400));
                            primaryStage.show();
                            screenLocY.set(primaryStage.getY() + 20);
                            screenLocX.set(primaryStage.getX() + 20);
                        }
                    
                        public int getLastNewLineCharBeforeCaret(String txt, int caretPos) {
                            if (caretPos == 0) {
                                return 0;
                            }
                            if (txt.isEmpty() || txt == null) {
                                return 0;
                            }
                            int counter = txt.lastIndexOf("\n");
                            if (counter == -1) {
                                return 0;
                            } else {
                                return counter;
                            }
                    
                        }
                    
                        public static void main(String[] args) {
                            launch(args);
                        }
                    }