JavaFX 17 -> Custom TextArea/TextField Right Click Menu

688 Views Asked by At

I would like to ask a small question. Indeed, I want to customize the menu that appears when we make a right click in a textarea or a textfield. My goal would be to keep the basic menu (copy, paste, cut...) by adding the buttons I want.

I found this post that explains how to do it: JavaFX Append to right click menu for TextField

import com.sun.javafx.scene.control.skin.TextFieldSkin;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;

public class GiveMeContext extends Application {
    @Override
    public void start(final Stage stage) throws Exception {
        TextField textField = new TextField();
        TextFieldSkin customContextSkin = new TextFieldSkin(textField) {
            @Override
            public void populateContextMenu(ContextMenu contextMenu) {
                super.populateContextMenu(contextMenu);
                contextMenu.getItems().add(0, new SeparatorMenuItem());
                contextMenu.getItems().add(0, new MenuItem("Register"));
            }
        };
        textField.setSkin(customContextSkin);

        stage.setScene(new Scene(textField));
        stage.show();
    }
    public static void main(String[] args) throws Exception {
        launch(args);
    }
}

After trying, it works perfectly well for java 8, but as they were talking about it at the time, after java 9, it doesn't work anymore.

I tried to replace the problematic method (populateContextMenu) but unfortunately I couldn't find any way.

I would be very thankful if someone shows me how to do it using java 9+

2

There are 2 best solutions below

3
Pavel_K On

Your code won't work in JavaFX 9+ because of modularization. For details read this. The only thing you can do is to use context menu and fill it with your own values. A full example to do it in JavaFX 17 is below.

Step 1. Create new project.

Pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.mycompany</groupId>
    <artifactId>mavenproject1</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.9.0</version>
            </plugin>
        </plugins>
    </build>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>
    
    <dependencies>
           <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>17.0.2-ea+2</version>
                <scope>compile</scope>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-base</artifactId>
                <version>17.0.2-ea+2</version>
                <scope>compile</scope>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-fxml</artifactId>
                <version>17.0.2-ea+2</version>
                <scope>compile</scope>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-graphics</artifactId>
                <version>17.0.2-ea+2</version>
                <scope>compile</scope>
            </dependency>
    </dependencies>
</project>

module-info:

module Mavenproject1 {
    requires javafx.controls;
    requires javafx.base;
    requires javafx.fxml;
    requires javafx.graphics;
    opens com.mycompany;
}

Main class:

package com.mycompany;


import javafx.scene.control.skin.TextFieldSkin;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;

public class NewMain2 extends Application {
    
    @Override
    public void start(final Stage stage) throws Exception {
        TextField textField = new TextField();
        
        ContextMenu contextMenu = new ContextMenu();
        MenuItem menuItem1 = new MenuItem("Choice 1");
        MenuItem menuItem2 = new MenuItem("Choice 2");
        MenuItem menuItem3 = new MenuItem("Choice 3");
        contextMenu.getItems().addAll(menuItem1, menuItem2, menuItem3);
        
        textField.setContextMenu(contextMenu);

        stage.setScene(new Scene(textField));
        stage.show();
    }
    public static void main(String[] args) throws Exception {
        launch(args);
    }
}

Step 2. Build you project.

Step 3. Download JavaFX SDK from here.

Step 4 Run you project this way

 java --module-path ./mavenproject1-1.0-SNAPSHOT.jar:/opt/javafx-sdk-17.0.2/lib --add-modules=javafx.controls,javafx.fxml -m Mavenproject1/com.mycompany.NewMain2
0
Silvio Barbieri On

After long hours of programming I found a way to "extend" the default context menu of a TextInputControl. I have to rebuild it from scratch, but it's not so complex as it may seem.

My code:

import java.util.Collection;
import java.util.ResourceBundle;
import java.util.function.Consumer;

import org.apache.commons.lang3.StringUtils;

import javafx.scene.control.ContextMenu;
import javafx.scene.control.IndexRange;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TextInputControl;

public interface JFXTextUtils {

    static void initializeContextMenu(TextInputControl textField) {
        final MenuItem undoMI = new ContextMenuItem("Undo", textField, TextInputControl::undo);
        final MenuItem redoMI = new ContextMenuItem("Redo", textField, TextInputControl::redo);
        final MenuItem cutMI = new ContextMenuItem("Cut", textField, TextInputControl::cut);
        final MenuItem copyMI = new ContextMenuItem("Copy", textField, TextInputControl::copy);
        final MenuItem pasteMI = new ContextMenuItem("Paste", textField, TextInputControl::paste);
        final MenuItem selectAllMI = new ContextMenuItem("SelectAll", textField, TextInputControl::selectAll);
        final MenuItem deleteMI = new ContextMenuItem("DeleteSelection", textField, JFXTextUtils::deleteSelectedText);

        textField.undoableProperty().addListener((obs, oldValue, newValue) -> undoMI.setDisable(!newValue));
        textField.redoableProperty().addListener((obs, oldValue, newValue) -> redoMI.setDisable(!newValue));
        textField.selectionProperty().addListener((obs, oldValue, newValue) -> {
            cutMI.setDisable(newValue.getLength() == 0);
            copyMI.setDisable(newValue.getLength() == 0);
            deleteMI.setDisable(newValue.getLength() == 0);
            selectAllMI.setDisable(newValue.getLength() == newValue.getEnd());
        });

        undoMI.setDisable(true);
        redoMI.setDisable(true);
        cutMI.setDisable(true);
        copyMI.setDisable(true);
        deleteMI.setDisable(true);
        selectAllMI.setDisable(true);

        textField.setContextMenu(ContextMenu(undoMI, redoMI, cutMI, copyMI, pasteMI, deleteMI, new SeparatorMenuItem(), selectAllMI,
                new SeparatorMenuItem()));
    }

    static void deleteSelectedText(TextInputControl t) {
        IndexRange range = t.getSelection();
        if (range.getLength() == 0) {
            return;
        }
        String text = t.getText();
        String newText = text.substring(0, range.getStart()) + text.substring(range.getEnd());
        t.setText(newText);
        t.positionCaret(range.getStart());
    }

    class ContextMenuItem extends MenuItem {
        ContextMenuItem(final String action, TextInputControl textField, Consumer<TextInputControl> function) {
            super(ResourceBundle.getBundle("com/sun/javafx/scene/control/skin/resources/controls")
                    .getString("TextInputControl.menu." + action));
            setOnAction(e -> function.accept(textField));
        }
    }

}

This code recreate exactly the default context menu and is ready to accept more MenuItem after the last MenuSeparator.