0

i am trying to include a suggestion box (similar to VSCode's code completion box) in richtext's codearea. But it faces major issues:

  1. cursor doesn't change when the mouse is inside the suggestion box, it remains I-beam
  2. scroll doesn't work when doing with mouse inside the suggestion box, instead it scrolls the codearea
  3. position of suggestion box must be below the caret position on next line (Y-axis) and layoutX must be just at the caret's X position.

java file

package com.example.notepad;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.ListView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.fxmisc.richtext.CodeArea;

import java.util.*;
import java.util.stream.Collectors;

public class suggestion_handler {

    public VBox codeContainer;
    @FXML private StackPane editorRoot;
    @FXML public ListView<String> suggestionList;

    private CodeArea codeArea;

    @FXML
    public void initialize() {
        // This makes the suggestionBox itself transparent for mouse events
        editorRoot.setMouseTransparent(false);
        suggestionList.setCursor(Cursor.DEFAULT);


        configureSuggestionList();
        editorRoot.setCursor(Cursor.TEXT);
        suggestionList.setCursor(Cursor.DEFAULT);
    }

    public void setCodeArea(CodeArea area) {
        this.codeArea = area;
        attachListeners();  // safe to call now, codeArea is assigned
    }

    private void configureSuggestionList() {
        suggestionList.setFixedCellSize(24);

        suggestionList.setFocusTraversable(false);

        suggestionList.setOnMouseReleased(event -> {
            acceptSelected();
            event.consume();
        });
        suggestionList.setOnScroll(event -> {
            if (codeArea != null) {
                codeArea.fireEvent(event.copyFor(codeArea, codeArea));
                event.consume();
            }
        });

        suggestionList.setOnKeyPressed(event -> {
            if (event.getCode() == KeyCode.ENTER) acceptSelected();
            else if (event.getCode() == KeyCode.ESCAPE) hideSuggestions();
        });
    }

    private boolean listenersAttached = false;

    private void attachListeners() {
        if (listenersAttached || codeArea == null) return;
        listenersAttached = true;

        codeArea.textProperty().addListener((_, _, _) -> Platform.runLater(this::showSuggestionsIfNeeded));
        codeArea.caretPositionProperty().addListener((_, _, _) -> Platform.runLater(this::showSuggestionsIfNeeded));

        codeArea.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
            if (suggestionList.isVisible()) {
                suggestionList.getSelectionModel().selectFirst();
                if (event.getCode() == KeyCode.UP) {
                    suggestionList.requestFocus();
                    suggestionList.getSelectionModel().selectFirst();
                    event.consume();
                } else if (event.getCode() == KeyCode.DOWN) {
                    suggestionList.requestFocus();
                    suggestionList.getSelectionModel().select(1);
                    event.consume();
                } else {
                    if (event.getCode() == KeyCode.ENTER) {
                        acceptSelected();
                        event.consume();
                    } else if (event.getCode() == KeyCode.ESCAPE) {
                        hideSuggestions();
                        event.consume();
                    }
                }
            }
            else codeArea.requestFocus();
        });
    }

    private void showSuggestionsIfNeeded() {
        String prefix = getCurrentWordPrefix();
        System.out.println(prefix);
        if (!prefix.isEmpty()) {
            List<String> matches = findMatches(prefix);
            if (!matches.isEmpty()) {
                suggestionList.getItems().setAll(matches);
                suggestionList.getSelectionModel().selectFirst();
                showSuggestions();
                positionSuggestionBox();
                return;
            }
        }
        hideSuggestions();
    }

    private String getCurrentWordPrefix() {
        int caretPos = codeArea.getCaretPosition();
        if (caretPos == 0) return "";
        String text = codeArea.getText(0, caretPos);
        int i = text.length() - 1;
        while (i >= 0 && Character.isLetterOrDigit(text.charAt(i))) i--;
        return text.substring(i + 1);
    }

    private List<String> findMatches(String prefix) {
        String text = codeArea.getText();
        Set<String> words = new HashSet<>(Arrays.asList(text.split("\\W+")));
        return words.stream()
                .filter(w -> !w.equals(prefix) && w.startsWith(prefix))
                .sorted()
                .collect(Collectors.toList());
    }

    private void positionSuggestionBox() {
        suggestionList.setPrefWidth(200);
        suggestionList.setPrefHeight(120);
        suggestionList.setMaxWidth(200);
        suggestionList.setMaxHeight(120);
        StackPane.setAlignment(suggestionList, Pos.TOP_LEFT); // necessary for layoutX/Y to apply

        if (codeArea == null || suggestionList == null) return;

        // Get the pixel position of the caret
        int caretPos = codeArea.getCaretPosition();

        try {
            // Get the bounds of the caret in the editor
            var caretBoundsOpt = codeArea.getCaretBounds();
            if (caretBoundsOpt.isEmpty()) return;

            var caretBounds = caretBoundsOpt.get();

            // Convert local bounds of CodeArea caret to parent (StackPane) coordinates
            var localToScene = codeArea.localToScene(caretBounds);
            var sceneToParent = editorRoot.sceneToLocal(localToScene);

            double x = localToScene.getMinX()+100;
            double y = localToScene.getMaxY()+20;

            suggestionList.setLayoutX(x);
            suggestionList.setLayoutY(y);
        } catch (Exception e) {
            System.err.println("Failed to position suggestion box: " + e.getMessage());
        }
    }

    private void acceptSelected() {
        String selected = suggestionList.getSelectionModel().getSelectedItem();
        if (selected != null) {
            selected+=" ";
            int caretPos = codeArea.getCaretPosition();
            int start = caretPos - 1;
            String text = codeArea.getText();
            while (start >= 0 && Character.isLetterOrDigit(text.charAt(start))) start--;
            start++;
            codeArea.replaceText(start, caretPos, selected);
            codeArea.moveTo(start + selected.length());
        }
        hideSuggestions();
        codeArea.requestFocus();
    }

    private void showSuggestions() {
        suggestionList.setVisible(true);
        suggestionList.setManaged(true);
        suggestionList.setOnMouseEntered(e ->{
            suggestionList.setCursor(Cursor.DEFAULT);
            suggestionList.requestFocus();
            editorRoot.setMouseTransparent(false);
        });
        suggestionList.setOnMouseExited(e ->{
            suggestionList.setCursor(Cursor.TEXT);
            codeArea.requestFocus();
            editorRoot.setMouseTransparent(true);
        });

        suggestionList.setCursor(Cursor.DEFAULT);
    }

    private void hideSuggestions() {
        editorRoot.setCursor(Cursor.TEXT);
        editorRoot.setMouseTransparent(true);
        suggestionList.setVisible(false);
        suggestionList.setManaged(false);
    }
}

FXML code:

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.control.ListView?>

<StackPane fx:id="editorRoot" xmlns:fx="http://javafx.com/fxml" fx:controller="com.example.notepad.suggestion_handler">
    <VBox fx:id="codeContainer"/>
        <ListView fx:id="suggestionList" visible="false" managed="false" />
</StackPane>
1
  • JavaFX 24 versions includes an incubator rich text editor. You could try that if you can't get the functionality you want from the org.fxmisc.richtext.CodeArea that you are currently using. You tagged the question as JavaFX 8, so if you are using that obsolete version of JavaFX and must stick with it, then the new inbuilt JavaFX rich text editor control won't work with that version and you can ignore this comment. Commented Jul 31 at 19:12

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.