0

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+

Pavel_K
  • 10,748
  • 13
  • 73
  • 186
Spinogl
  • 15
  • 6

2 Answers2

1

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
Pavel_K
  • 10,748
  • 13
  • 73
  • 186
  • Thank you very much for your comprehensive answer Pavel_K What you have given me works very well, as do other solutions I have found elsewhere Now I admit that my question was specifically about keeping the basic menu while adding my own options. In the link I shared above, the person was able to get the existing base menu and add their own options. The problem is that it doesn't work since Java 9 and I haven't found anything to replace it Is it impossible to get the basic right-click menu to customize it after Java 9? Hoping you know something that might match what I'm looking for – Spinogl Feb 11 '22 at 07:47
  • @Spinogl No, after Java 9 you can't use that old basic menu. You need to create it yourself from the scratch. Just do it yourself, there is nothing difficult (copy, paste etc is not difficult to implement). – Pavel_K Feb 11 '22 at 08:31
  • @Spinogl If you can't, then say, I will show how to do it. – Pavel_K Feb 11 '22 at 08:34
1

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.