Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX 2 TextArea: How to stop it from consuming [Enter] and [Tab]

I want to use a JavaFX TextArea as though it were exactly like a multi-line TextField. In other words, when I press [Tab] I want to cycle to the next control on the form and when I press [Enter] I want the Key.Event to go to the defaultButton control (rather than be consumed by the TextArea).

The default behavior for TextArea is that [Tab] gets inserted into the TextArea and [Enter] inserts a new-line character.

I know that I need to use EventFilters to get the behavior that I want, but I'm getting it all wrong. I don't want the TextArea to consume these events ... I just want it to let them "go right on by".

like image 896
scottb Avatar asked May 20 '13 14:05

scottb


1 Answers

The solution here displays two text areas and a default button. When the user presses the tab key, the focus moves to the next control down. When the user presses the enter key, the default button is fired.

To achieve this behavior:

  1. The enter key press for each text area is caught in an event filter, copied and targeted to the text area's parent node (which contains the default OK button). This causes the default OK button to be fired when enter is pressed anywhere on the form. The original enter key press is consumed so that it does not cause a new line to be added to the text area's text.
  2. The tab key press for each text area is caught in a filter and the parent's focus traversable list is processed to find the next focusable control and focus is requested for that control. The original tab key press is consumed so that it does not cause new tab spacing to be added to the text area's text.

The code makes use of features implemented in Java 8, so Java 8 is required to execute it.

textareahandler

import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.value.*;
import javafx.collections.ObservableList;
import javafx.event.*;
import javafx.scene.*;
import javafx.scene.control.*;
import static javafx.scene.input.KeyCode.ENTER;
import static javafx.scene.input.KeyCode.TAB;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.*;

public class TextAreaTabAndEnterHandler extends Application {
  final Label status = new Label();

  public static void main(String[] args) { launch(args); }

  @Override public void start(final Stage stage) {
    final TextArea textArea1 = new TabAndEnterIgnoringTextArea();
    final TextArea textArea2 = new TabAndEnterIgnoringTextArea();

    final Button defaultButton = new Button("OK");
    defaultButton.setDefaultButton(true);
    defaultButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override public void handle(ActionEvent event) {
        status.setText("Default Button Pressed");
      }
    });

    textArea1.textProperty().addListener(new ClearStatusListener());
    textArea2.textProperty().addListener(new ClearStatusListener());

    VBox layout = new VBox(10);
    layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10px;");
    layout.getChildren().setAll(
      textArea1, 
      textArea2, 
      defaultButton, 
      status
    );

    stage.setScene(
      new Scene(layout)
    );
    stage.show();
  }

  class ClearStatusListener implements ChangeListener<String> {
    @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
      status.setText("");
    }
  }

  class TabAndEnterIgnoringTextArea extends TextArea {
    final TextArea myTextArea = this;

    TabAndEnterIgnoringTextArea() {
      addEventFilter(KeyEvent.KEY_PRESSED, new TabAndEnterHandler());
    }

    class TabAndEnterHandler implements EventHandler<KeyEvent> {
      private KeyEvent recodedEvent;

      @Override public void handle(KeyEvent event) {
        if (recodedEvent != null) {
          recodedEvent = null;
          return;
        }

        Parent parent = myTextArea.getParent();
        if (parent != null) {
          switch (event.getCode()) {
            case ENTER:
              if (event.isControlDown()) {
                recodedEvent = recodeWithoutControlDown(event);
                myTextArea.fireEvent(recodedEvent);
              } else {
                Event parentEvent = event.copyFor(parent, parent);
                myTextArea.getParent().fireEvent(parentEvent);
              }  
              event.consume();
              break;

            case TAB:
              if (event.isControlDown()) {
                recodedEvent = recodeWithoutControlDown(event);
                myTextArea.fireEvent(recodedEvent);
              } else {
                ObservableList<Node> children = parent.getChildrenUnmodifiable();
                int idx = children.indexOf(myTextArea);
                if (idx >= 0) {
                  for (int i = idx + 1; i < children.size(); i++) {
                    if (children.get(i).isFocusTraversable()) {
                      children.get(i).requestFocus();
                      break;
                    }
                  }
                  for (int i = 0; i < idx; i++) {
                    if (children.get(i).isFocusTraversable()) {
                      children.get(i).requestFocus();
                      break;
                    }
                  }
                }
              }  
              event.consume();
              break;
          }
        }  
      }

      private KeyEvent recodeWithoutControlDown(KeyEvent event) {
        return new KeyEvent(
          event.getEventType(), 
          event.getCharacter(), 
          event.getText(), 
          event.getCode(), 
          event.isShiftDown(), 
          false, 
          event.isAltDown(), 
          event.isMetaDown()
        );
      }
    }
  }
}

An alternate solution would be to implement your own customized skin for TextArea which includes new key handling behavior. I believe that such a process would be more complicated than the solution presented here.

Update

One thing I didn't really like about my original solution to this problem was that once the Tab or Enter key was consumed, there was no way to trigger their default processing. So I updated the solution such that if the user holds the control key down when pressing Tab or Enter, the default Tab or Enter operation will be performed. This updated logic allows the user to insert a new line or tab space into the text area by pressing CTRL+Enter or CTRL+Tab.

like image 78
jewelsea Avatar answered Nov 03 '22 00:11

jewelsea