bekwam courses

Easy JavaFX Visual Cues for the Deaf and Hard of Hearing

November 12, 2016

This article presents a JavaFX application which contains audible cues that sound when an invalid character -- a letter -- is entered in a TextField expecting only numbers. This is subtle way to remind the user about the expected values without interrupting their concentration with a message or a popup.

Those with hearing impairments might not hear the sounds. If they continue to enter invalid characters, and do not receive feedback from the application, they might assume that their keyboard is not functioning or even that the application is broken. This article will present a secondary notification mechanism that is visible. Through an application configuration, the user can set his or her preference and receive audible, visual, both audible and visual, or neither audibile nor visual cues.

The technique for visual cues is a JavaFX-only solution that is easy to implement, regardless of layout. Often times, accessibility added to an application in a later version. This Stage-only technique will work for any layout and would not have a signifant cost or effect on a deadline.

This is a video demonstration of the program. The video is not narrated, but closed captioning is available for when the program emits the audible beeps.

The article includes a Gradle project for building the code. See the Resources section at the end of the article.

UI Controls

The UI controls consist of a Menu, Label, TextField, and Button. The Menu contains boolean settings for Audible Cues and Visual Cues. Both can be set and both can be cleared.

The Label, TextField, and Button make up the form. Entering a number and pressing the Submit Button will print out the value in the TextField. This demonstration involves one Java class.  This is the beginning of the class which is a JavaFX Application subclass.

public class VisualCueApp extends Application {

    private CheckMenuItem visualItem = new CheckMenuItem("Visual Cues");
    private CheckMenuItem audibleItem = new CheckMenuItem("Audible Cues");
    private Stage stage = null;

    @Override
    public void start(Stage primaryStage) throws Exception {

        this.stage = primaryStage;

        VBox vbox = new VBox();

        MenuBar menubar = new MenuBar();
        Menu appMenu = new Menu("App");
        audibleItem.setSelected(true);
        appMenu.getItems().addAll( visualItem, audibleItem );
        menubar.getMenus().add( appMenu );

        VBox content = new VBox();
        content.setAlignment( Pos.CENTER_LEFT );
        content.setPadding( new Insets(40) );
        content.setSpacing( 10 );

        VBox.setVgrow(content, Priority.ALWAYS);

        Label label = new Label("Just Numbers");
        TextField tf = new TextField();

        Button b = new Button("Submit");

        content.getChildren().addAll( label, tf, b );

        vbox.getChildren().addAll( menubar, content );
        

TextFormatter

The TextField is equipped with a TextFormatter which will coerce the input into a numeric format. If the character entered is not a number and not a control character like a backspace, the input will be discarded and nothing will be entered in the TextField. The code to provide the warnings will be presented in the next section.

        TextFormatter<Integer> formatter = new TextFormatter<>(
                new IntegerStringConverter(),
                null,
                (change) ->  {
                        String text = change.getText();
                        for (int i = 0; i < text.length(); i++) {
                            if (!Character.isDigit(text.charAt(i)))
                                return null;
                        }
                        return change;

                    }
                );

        tf.setTextFormatter(formatter);
        tf.addEventFilter(KeyEvent.KEY_PRESSED, keyEventHandler);

        b.setOnAction( (evt) -> System.out.println( formatter.getValue() ));

        Scene scene = new Scene( vbox );

        primaryStage.setTitle("Visual Cue App");
        primaryStage.setScene( scene );
        primaryStage.setWidth(480);
        primaryStage.setHeight(320);

        primaryStage.show();
    }
  

The TextFormatter also enables the TextField input to be retrieved as the proper type. In the Button setOnAction() handler, the formatter.getValue() returns the TextField input as an Integer rather than a String.

EventFilter

An EventFilter is used to activate the notifications in the case of bad (letter) input. The EventFilter works with the TextFormatter to process a key press. The EventFilter examines the key that was pressed and decides whether or not to warn the user. In all cases, the key press is handed-off to the TextFormatter which runs later and decides whether or not to apply the change.

This is the code which is added to the TextField.

    private EventHandler<KeyEvent> keyEventHandler = (evt) -> {

        if( !evt.getCode().isDigitKey() &&
                !evt.getCode().isArrowKey() &&
                !evt.getCode().isFunctionKey() &&
                !evt.getCode().isModifierKey() &&
                !evt.getCode().equals(KeyCode.BACK_SPACE) &&
                !evt.getCode().equals(KeyCode.DELETE) &&
                !evt.getCode().equals(KeyCode.TAB) &&
                !evt.getCode().equals(KeyCode.ENTER) &&
                !evt.isControlDown()) {

            if( audibleItem.isSelected() ) {
                giveAudibleCue();
            }
            if( visualItem.isSelected() ) {
                giveVisualCue();
            }
        }
    };
	

Audible Cue

The audible cue is provided by a Java AWT call to beep(). This works on Windows and Mac. However, I found that Ubuntu 16.04 had this beep() turned off so no sound was produced when an error was encountered. The project repacks a Ubuntu .wav and has an OS-specific hack to make this work across all platforms.

    private void giveAudibleCue() {
        if (isLinux()) {
            ubuntuBeep();
        } else {
            Platform.runLater(() -> Toolkit.getDefaultToolkit().beep() );
        }
    }

    private void ubuntuBeep() {
        try {
            AudioClip beepSound = new AudioClip(
                    this.getClass().getResource(
                    	"/visualcueapp/KDE-Sys-App-Error.wav"
					).toString()
            );
            beepSound.play();
        } catch(Exception ignore) {}
    }

    private boolean isLinux() {
        String osName = System.getProperty("os.name");
        return osName != null && osName.startsWith("Linux");
    }
	

Visual Cue

The Visual Cue is provided by briefly making the Stage slightly transparent and then resetting the Stage to be fully opaque. This is done with a JavaFX Animation.

    private void giveVisualCue() {

        if( stage != null ) {

            final Timeline timeline = new Timeline();

            final KeyValue kv = new KeyValue(stage.opacityProperty(), 0.7f);
            final KeyFrame kf = new KeyFrame(Duration.millis(300), kv);

            timeline.getKeyFrames().add( kf );
            timeline.setOnFinished( (evt) -> stage.setOpacity(1.0f) );

            timeline.play();
        }
    }

The visual cue could be stronger or more stylish, but the purpose of the article is to show something that can be used easily in any JavaFX application. The key is to encourage putting something in place to assist users with a hearing impairment even when deadlines are tight. Every JavaFX UI has a Stage and (unfortunately) the properties available to the programmer are limited.  A better implementation will give even a stronger cue although that will have more of an effect on the application.

Resources

The source code presented in this video series is a Gradle project that can be imported into your IDE.

VisualCueApp Source Zip (248Kb)
Headshot of Carl Walker

By Carl Walker

President and Principal Consultant of Bekwam, Inc