javajavafx

Javafx NodeOrientation does not work as expected


I'm developing an app for Arabic users so i have set:

root.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);

This works fine when text fields contain right-to-left text, e.g., Arabic. However, it breaks the caret logic (moving the caret using keyboard left/right arrows) whenever the field contains numbers or Latin text. Here is a demo:

import javafx.application.Application;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;


public class NodeOrientationDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        TextField numbersTextField = new TextField("0123456789");
        numbersTextField.setMaxSize(80, 30);

        TextField latinTextField = new TextField("Hello");
        latinTextField.setMaxSize(80, 30);

        TextField arabicTextField = new TextField("مرحبا");
        arabicTextField.setMaxSize(80, 30);

        VBox root = new VBox(10, numbersTextField, latinTextField, arabicTextField);
        root.setAlignment(Pos.CENTER);
        root.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);

        primaryStage.setScene(new Scene(root, 400, 400));
        primaryStage.setTitle("NodeOrientation");
        primaryStage.show();
    }
}

Is this a known bug? And how can I solve it?

Any help is appreciated, thanks in advance!

Update 1:

I have added event filters to all the text fields and they seem to work fine. However, only the Arabic text navigation is reversed (left should be right, right should be left) my plan is to detect if it is Arabic text and based on that I will add/subtract the caret position.

import javafx.application.Application;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;


public class NodeOrientationDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        TextField numbersTextField = new TextField("0123456789");
        numbersTextField.setMaxSize(80, 30);
        keyboardNavigation(numbersTextField);

        TextField latinTextField = new TextField("Hello");
        latinTextField.setMaxSize(80, 30);
        keyboardNavigation(latinTextField);

        TextField arabicTextField = new TextField("مرحبا");
        arabicTextField.setMaxSize(80, 30);
        keyboardNavigation(arabicTextField);

        VBox root = new VBox(10, numbersTextField, latinTextField, arabicTextField);
        root.setAlignment(Pos.CENTER);
        root.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);

        primaryStage.setScene(new Scene(root, 400, 400));
        primaryStage.setTitle("NodeOrientation");
        primaryStage.show();
    }

    private void keyboardNavigation(TextField textField) {
        textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
            if (event.getCode() == KeyCode.LEFT) {
                textField.positionCaret(textField.getCaretPosition() - 1);
                event.consume();
            } else if (event.getCode() == KeyCode.RIGHT) {
                textField.positionCaret(textField.getCaretPosition() + 1);
                event.consume();
            }
        });
    }
}

Update 2:

I have implemented the plan in update 1 and it looks good. However, one remaining issue when the text field contains both Arabic text and Latin/numbers then the behavior is a bit unclear.


import javafx.application.Application;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;


public class NodeOrientationDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        TextField numbersTextField = new TextField("0123456789");
        numbersTextField.setMaxSize(80, 30);
        keyboardNavigation(numbersTextField);

        TextField latinTextField = new TextField("Hello");
        latinTextField.setMaxSize(80, 30);
        keyboardNavigation(latinTextField);

        TextField arabicTextField = new TextField("مرحبا");
        arabicTextField.setMaxSize(80, 30);
        keyboardNavigation(arabicTextField);

        VBox root = new VBox(10, numbersTextField, latinTextField, arabicTextField);
        root.setAlignment(Pos.CENTER);
        root.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);

        primaryStage.setScene(new Scene(root, 400, 400));
        primaryStage.setTitle("NodeOrientation");
        primaryStage.show();
    }

    private void keyboardNavigation(TextField textField) {
        textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
            final int pos = clamp(1, textField.getCaretPosition(), textField.getLength());
            final boolean isArabic = isArabicCharacter(textField.getText().charAt(pos - 1));
            if (event.getCode() == KeyCode.LEFT) {
                textField.positionCaret(textField.getCaretPosition() + (isArabic ? 1 : -1));
                event.consume();
            } else if (event.getCode() == KeyCode.RIGHT) {
                textField.positionCaret(textField.getCaretPosition() + (isArabic ? -1 : 1));
                event.consume();
            }
        });
    }

    public static boolean isArabicCharacter(char c) {
        return Character.UnicodeBlock.of(c) == Character.UnicodeBlock.ARABIC;
    }

    public static int clamp(int min, int value, int max) {
        return value < min ? min : Math.min(value, max);
    }
}

Solution

  • I foolishly thought update 2 (in the question) would do the trick, however, that wasn't the case. According to what I have tried, you simply cannot get rid of the "revers logic". Instead, you either have it in the Arabic text or vice versa. The following code did the job for me:

    import javafx.application.Application;
    import javafx.geometry.NodeOrientation;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.TextField;
    import javafx.scene.input.KeyCode;
    import javafx.scene.input.KeyEvent;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import org.jetbrains.annotations.NotNull;
    
    
    public class NodeOrientationDemo extends Application {
        @Override
        public void start(Stage primaryStage) {
            TextField numbersTextField = new TextField("0123456789");
            numbersTextField.setMaxSize(80, 30);
            keyboardNavigation(numbersTextField);
    
            TextField latinTextField = new TextField("Hello");
            latinTextField.setMaxSize(80, 30);
            keyboardNavigation(latinTextField);
    
            TextField arabicTextField = new TextField("مرحبا");
            arabicTextField.setMaxSize(80, 30);
            keyboardNavigation(arabicTextField);
    
            VBox root = new VBox(10, numbersTextField, latinTextField, arabicTextField);
            root.setAlignment(Pos.CENTER);
            root.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);
    
            primaryStage.setScene(new Scene(root, 400, 400));
            primaryStage.setTitle("NodeOrientation");
            primaryStage.show();
        }
    
        private void keyboardNavigation(@NotNull TextField textField) {
            textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
                final KeyCode code = event.getCode();
                if (code == KeyCode.LEFT || code == KeyCode.RIGHT) {
                    boolean containsArabicText = false;
                    for (char c : textField.getText().toCharArray()) {
                        if (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.ARABIC) {
                            containsArabicText = true;
                            break;
                        }
                    }
                    textField.positionCaret(textField.getCaretPosition() + (containsArabicText ? 1 : -1) * (code == KeyCode.RIGHT ? -1 : 1));
                    event.consume();
                }
            });
        }
    }