6

I am looking to create an editable label at an arbitrary position on the pane on which I am writing. I am under the impression that TextField or TextArea objects are what I could use to implement that capability. There is obviously more to it as I don't know how to position the object when I create it. I have found an example on the "Chaotic Java" website but I need to do a bit more work to understand what's going on there. http://chaoticjava.com/posts/another-javafx-example-the-editable-label/

I am looking for more input from this group.

(There are no errors because I have not written any code.)

  • 1
    What's is the root ? AnchorPane ? Pane ? Your question is too broad now. Change it and add an [mcve](http://stackoverflow.com/help/mcve) that reproduces the error. – Mansueli Aug 29 '14 at 16:30
  • I'm not clear how an "editable label" is different to a `TextField`. Why not just use a `TextField`? – James_D Aug 29 '14 at 17:19
  • Imagine simply wanting to type text anywhere in a window in which you are working without it being associated with any other control. Later I may want to edit that text, so I click on the text and voila! the text becomes editable. – Garrett A. Hughes Aug 29 '14 at 17:31
  • Well, that's quite complex, and SO is not a code service, per se, so you will have to try something and you can use these forums to seek help if your code is not behaving as expected. I would start with a `Pane`, use `Text` objects to represent the static text, and `TextFields` to represent text while being edited. Register mouse listeners with the `Pane` and `Text` objects, and use the `layoutX` and `layoutY` properties to position things. – James_D Aug 29 '14 at 17:45
  • Another approach might be just to use text fields, and to use CSS to make them look like labels when not focused and text fields when focused. – James_D Aug 29 '14 at 18:53
  • The chaoticjava link in the question is JavaFX 1.x code, which is completely obsolete and uses a completely different programming language for JavaFX 2.2+. – jewelsea Aug 29 '14 at 18:59
  • Thanks for the input guys. Text objects look promising. I will give James_D's suggestions a go. I would have thought a straightforward, movable, editable text box with a transparent background would be a key item in any graphics package. – Garrett A. Hughes Aug 29 '14 at 19:58

2 Answers2

7

I was kind of curious about how to achieve this, so I gave it a try. This is what I came up with.

lonely

The approach used is pretty the same as that suggested by James in his comment:

I would start with a Pane, . . ., TextFields to represent text while being edited. Register mouse listeners with the Pane and Text objects, and use the layoutX and layoutY properties to position things . . . just to use text fields, and to use CSS to make them look like labels when not focused and text fields when focused.

The only significantly tricky part was working out how to correctly size the text fields as the Text inside the text field is not exposed via public API to allow you to listen to it's layout bounds. You could perhaps use a css lookup function to get at the enclosed Text, but I chose to use a private sun FontMetrics API (which may be deprecated in the future), to get the size of the text. In the future with Java 9, you should be able to perform the task without using the private API.

The solution doesn't try to do anything tricky like deal with multi-format or multi-line text, it is just for short, single line comments of a few words that can be placed over a scene.

TextCreator.java

// ## CAUTION: beware the com.sun imports...
import com.sun.javafx.tk.FontMetrics;
import com.sun.javafx.tk.Toolkit;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

/**
 * Displays a map of the lonely mountain upon which draggable, editable labels can be overlaid.
 */
public class TextCreator extends Application {
    private static final String MAP_IMAGE_LOC =
            "http://images.wikia.com/lotr/images/archive/f/f6/20130209175313!F27c_thorins_map_from_the_hobbit.jpg";

    public static void main(String[] args) throws Exception {
        launch(args);
    }

    @Override
    public void start(final Stage stage) throws Exception {
        Pane pane = new Pane();

        pane.setOnMouseClicked(event -> {
            if (event.getTarget() == pane) {
                pane.getChildren().add(
                        new EditableDraggableText(event.getX(), event.getY())
                );
            }
        });

        EditableDraggableText cssStyled = 
                new EditableDraggableText(439, 253, "Style them with CSS");
        cssStyled.getStyleClass().add("highlighted");

        pane.getChildren().addAll(
                new EditableDraggableText(330, 101, "Click to add a label"),
                new EditableDraggableText(318, 225, "You can edit your labels"),
                cssStyled,
                new EditableDraggableText(336, 307, "And drag them"),
                new EditableDraggableText(309, 346, "Around The Lonely Mountain")
        );

        StackPane layout = new StackPane(
            new ImageView(
                    new Image(
                            MAP_IMAGE_LOC
                    )
            ),
            pane
        );

        Scene scene = new Scene(layout);
        scene.getStylesheets().add(getClass().getResource(
            "editable-text.css"
        ).toExternalForm());

        stage.setScene(scene);
        stage.setResizable(false);
        stage.show();
    }

    /**
     * A text field which has no special decorations like background, border or focus ring.
     *   i.e. the EditableText just looks like a vanilla Text node or a Label node.
     */
    class EditableText extends TextField {
        // The right margin allows a little bit of space
        // to the right of the text for the editor caret.
        private final double RIGHT_MARGIN = 5;

        EditableText(double x, double y) {
            relocate(x, y);
            getStyleClass().add("editable-text");

            //** CAUTION: this uses a non-public API (FontMetrics) to calculate the field size
            //            the non-public API may be removed in a future JavaFX version.
            // see: https://bugs.openjdk.java.net/browse/JDK-8090775
            //      Need font/text measurement API
            FontMetrics metrics = Toolkit.getToolkit().getFontLoader().getFontMetrics(getFont());
            setPrefWidth(RIGHT_MARGIN);
            textProperty().addListener((observable, oldTextString, newTextString) ->
                setPrefWidth(metrics.computeStringWidth(newTextString) + RIGHT_MARGIN)
            );

            Platform.runLater(this::requestFocus);
        }
    }

    /**
     * An EditableText (a text field which looks like a label), which can be dragged around
     * the screen to reposition it.
     */
    class EditableDraggableText extends StackPane {
        private final double PADDING = 5;
        private EditableText text = new EditableText(PADDING, PADDING);

        EditableDraggableText(double x, double y) {
            relocate(x - PADDING, y - PADDING);
            getChildren().add(text);
            getStyleClass().add("editable-draggable-text");

            // if the text is empty when we lose focus,
            // the node has no purpose anymore
            // just remove it from the scene.
            text.focusedProperty().addListener((observable, hadFocus, hasFocus) -> {
                if (!hasFocus && getParent() != null && getParent() instanceof Pane &&
                    (text.getText() == null || text.getText().trim().isEmpty())) {
                    ((Pane) getParent()).getChildren().remove(this);
                }
            });

            enableDrag();
        }

        public EditableDraggableText(int x, int y, String text) {
            this(x, y);
            this.text.setText(text);
        }

        // make a node movable by dragging it around with the mouse.
        private void enableDrag() {
            final Delta dragDelta = new Delta();
            setOnMousePressed(mouseEvent -> {
                this.toFront();
                // record a delta distance for the drag and drop operation.
                dragDelta.x = mouseEvent.getX();
                dragDelta.y = mouseEvent.getY();
                getScene().setCursor(Cursor.MOVE);
            });
            setOnMouseReleased(mouseEvent -> getScene().setCursor(Cursor.HAND));
            setOnMouseDragged(mouseEvent -> {
                double newX = getLayoutX() + mouseEvent.getX() - dragDelta.x;
                if (newX > 0 && newX < getScene().getWidth()) {
                    setLayoutX(newX);
                }
                double newY = getLayoutY() + mouseEvent.getY() - dragDelta.y;
                if (newY > 0 && newY < getScene().getHeight()) {
                    setLayoutY(newY);
                }
            });
            setOnMouseEntered(mouseEvent -> {
                if (!mouseEvent.isPrimaryButtonDown()) {
                    getScene().setCursor(Cursor.HAND);
                }
            });
            setOnMouseExited(mouseEvent -> {
                if (!mouseEvent.isPrimaryButtonDown()) {
                    getScene().setCursor(Cursor.DEFAULT);
                }
            });
        }

        // records relative x and y co-ordinates.
        private class Delta {
            double x, y;
        }
    }    
}

editable-text.css

.editable-text {
    -fx-background-color: transparent;
    -fx-background-insets: 0;
    -fx-background-radius: 0;
    -fx-padding: 0;
}

.editable-draggable-text:hover .editable-text {
    -fx-background-color: yellow;
}

.editable-draggable-text {
    -fx-padding: 5;
    -fx-background-color: rgba(152, 251, 152, 0.2); // translucent palegreen
}

.editable-draggable-text:hover {
    -fx-background-color: orange;
}

.highlighted {
    -fx-background-color: rgba(255, 182, 93, 0.3);  // translucent mistyrose
    -fx-border-style: dashed;
    -fx-border-color: firebrick;
}

If you have time, you could clean the sample implementation up and donate it to the ControlsFX project.

silvalli
  • 295
  • 2
  • 13
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • Superb, +1 !If someone is willing/not willing to improve this, please let me know. It would be great to work partially/fully on this ;) – ItachiUchiha Aug 30 '14 at 07:35
  • I edited the css file to use /* */ comments. The hand cursor and dragging didn't work until I realized that the css file was mangled by IDEA when I pasted in the file contents. JavaFX didn't complain when parsing the css file. The manglement looked like this: ``` -fx-background-color: rgba(255, 182, 93, 0.3); / / translucent mistyrose -fx-border-style: dashed; ``` – silvalli Aug 01 '19 at 05:23
  • from http://hg.openjdk.java.net/openjfx/2.2/master/rt/file/893db73acfb5/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/Utils.java – silvalli Aug 02 '19 at 22:14
3

You can use a function of label: setGraphic().

Here is my code:

public void editableLabelTest(Stage stage){
    Scene scene = new Scene(new VBox(new EditableLabel("I am a label"),
                                    new EditableLabel("I am a label too")));
    stage.setScene(scene);
    stage.show();
}

class EditableLabel extends Label{
    TextField tf = new TextField();
    /***
     * backup is used to cancel when press ESC...
     */
    String backup = "";
    public EditableLabel(){
        this("");
    }
    public EditableLabel(String str){
        super(str);
        this.setOnMouseClicked(e -> {
            if(e.getClickCount() == 2){
                tf.setText(backup = this.getText());
                this.setGraphic(tf);
                this.setText("");
                tf.requestFocus();
            }
        });
        tf.focusedProperty().addListener((prop, o, n) -> {
            if(!n){
                toLabel();
            }
        });
        tf.setOnKeyReleased(e -> {
            if(e.getCode().equals(KeyCode.ENTER)){
                toLabel();
            }else if(e.getCode().equals(KeyCode.ESCAPE)){
                tf.setText(backup);
                toLabel();
            }
        });
    }

    void toLabel(){
        this.setGraphic(null);
        this.setText(tf.getText());
    }

}
Stephen Kennedy
  • 20,585
  • 22
  • 95
  • 108
  • Ideally the TextField text should be positioned the same as the Label text, and the TextArea should be exactly the same size as the Label. I poked around a lot, and I can't see how. – silvalli Jul 31 '19 at 19:52
  • I would argue that `setOnKeyReleased` should be `setOnKeyPressed`. Even if you consume the enter event, it might be passed on to, for example, a default button when using `setOnKeyReleased`. – Marv Aug 13 '19 at 14:23