Controller Processing

I initialize all ChoiceBox entries with a ‘hardcoded’ list of values, and set the displayed value to the first item in the list.  It would be logical to do this initialization in SceneBuilder, but the tool only seems capable of creating a default set of values (named Item1, Item2, and Item3).  Here is an example for the startMonthSelect field.

   /**
     * Initializes all field selection values and sets defaults.
     */
    private void initializeFields() {
        startMonthSelect.setItems(FXCollections.observableArrayList(
            "Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"));
        startMonthSelect.getSelectionModel().selectFirst();
 
        (other ChoiceBox selection fields are handled in the same fashion…)
    }

Listing 5 – Initializing ChoiceBox Selection List Example

Event Handling

One of the key functions of the Controller is to properly handle events to respond to user actions in a natural way.  There are several different types of events that I need to respond to:

  • User selects a ChoiceBox value.  The ChoiceBoxes (sometimes called SelectionBox or Dropdown Menu) restrict user selections to a pre-defined range of values; for example, the Start Month must be in the range of “Jan” … “Dec”.
  • User enters a character (including a character key, digit key, Backspace, Delete, etc.,) in one of the Year fields.
  • User selects or deselects one of the “Use Current Date/Time” CheckBoxes.
  • User selects an item from one of the Menus (currently only the File menu has selectable items).
  • User clicks the “Calculate”, “Reset”, or “Close” button.

Most of the event handling is straightforward – typically I call a private method to respond to a user clicking a button or selecting a menu item. I did customize a few events as follows (see Listing 6):

When navigating through the Start Date and Finish Date in the entry panes, the up/down arrow key default behavior is to move to a different field in the pane, or to the next object in the controller hierarchy.  I overrode this behavior for these two keys so that if the cursor is located in a ChoiceBox, the up arrow key scrolls upward through the ChoiceBox selection list, and the down arrow key scrolls downward.  To do this I added a handler for the appropriate KeyCodes (UP and DOWN) in the KeyEvent.KEY_PRESSED event.  Note that the entry panes (startDTPane, finishDTPane) are the parents of the ChoiceBoxes within the pane.  By consuming the event at the parent level, the behavior propagates down to the child ChoiceBox.

I also overrode the up and down arrow key behavior in the Year fields, so that pressing the UP key decrements the year field (until the minimum Year is reached), and pressing the DOWN key increments the year field (until the maximum Year is reached).

    /**
     * Overrides the UP and DOWN KEY_PRESSED events in the date entry
     * choice boxes so that choice options are displayed.
     * Overrides the UP and DOWN KEY_RELEASED events in the YEAR
     * fields so that the year decrements or increments, respectively.
     * On detection of a KEY_RELEASED event in the YEAR fields,
     * ensures that the entered value is numeric, and the field value contains
     * a maximum of four digits.
     */
    private void setNavigationAids() {
        startDTPane.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler() {
            @Override public void handle(KeyEvent event) {
                if (event.getCode() == KeyCode.UP || event.getCode() == KeyCode.DOWN) {
                    event.consume();
                }
            }
        });
        finishDTPane.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler() {
            @Override public void handle(KeyEvent event) {
                if (event.getCode() == KeyCode.UP || event.getCode() == KeyCode.DOWN) {
                    event.consume();
                }
            }
        });
        startYearSelect.addEventFilter(KeyEvent.KEY_RELEASED, new EventHandler() {
            @Override public void handle(KeyEvent event) {
                if (!startYearSelect.getText().isEmpty()) {
                    if (event.getCode() == KeyCode.UP) {
                        int startYear = Integer.parseInt(startYearSelect.getText().trim()) - 1;
                        if (startYear < minYear) startYear = minYear;
                        startYearSelect.setText(Integer.toString(startYear));
                    }
                    else if (event.getCode() == KeyCode.DOWN) {
                        int startYear = Integer.parseInt(startYearSelect.getText().trim()) + 1;
                        if (startYear > maxYear) startYear = maxYear;
                        startYearSelect.setText(Integer.toString(startYear));
                    }
                    else {
                        int carentPos = startYearSelect.getCaretPosition();
                        String validContent = checkYearValue(startYearSelect.getText());
                        startYearSelect.setText(validContent);
                        startYearSelect.positionCaret(carentPos);
                    }                   
                }
            }
        });
        finishYearSelect.addEventFilter(KeyEvent.KEY_RELEASED, new EventHandler() {
            @Override public void handle(KeyEvent event) {
                    KeyCode key = event.getCode();
                    if (key == KeyCode.UP && !finishYearSelect.getText().isEmpty()) {
                        int startYear = Integer.parseInt(finishYearSelect.getText().trim()) - 1;
                        if (startYear < minYear) startYear = minYear;
                        finishYearSelect.setText(Integer.toString(startYear));
                    }
                    else if (key == KeyCode.DOWN && !finishYearSelect.getText().isEmpty()) {
                        int startYear = Integer.parseInt(finishYearSelect.getText().trim()) + 1;
                        if (startYear > maxYear) startYear = maxYear;
                        finishYearSelect.setText(Integer.toString(startYear));
                    }
                    else {
                        int caretPos = finishYearSelect.getCaretPosition();
                        String validContent = checkYearValue(finishYearSelect.getText());
                        finishYearSelect.setText(validContent);
                        finishYearSelect.positionCaret(caretPos);
                    }
                }
        });
    }

Listing 6 – Custom Event Handling

One last thing; I wanted the Year fields to be restricted to numeric values between minYear and maxYear. I started by creating a subclass of TextField called ConstrainedNumericTextField which did a simple override of methods in the parent class to ignore non-digit characters, and restrict the overall length of the field input to four characters.  But I ran into a problem using SceneBuilder because it does not appear to support the concept of subclassing the standard JavaFX controls (such as TextField).  I could update the FXML manually to refer to the subclasses, but I did not wish to manually edit the DateCalculatorUI.fxml file every time I used SceneBuilder.  As a consequence, I added a few lines to the KeyEvent.KEY_RELEASED code to check the user input and ignore non-numeric entries and numeric entries beyond four digits (Listing 7).  That also required tweaking the position of the caret (cursor) in the event handler.

    /**
     * Checks the content of the input String and suppresses non-numeric content.
     * Also ensures the length of the String is four digits or fewer.
     * Used to validate the content of the YEAR TextFields.
     * @param content Value of the input String.
     * @return updated value after removing non-numeric characters, and trimming
     * the length to a maximum of four digits.
     */
    private String checkYearValue(String content) {
        String rev = "";
        for (int i=0; i<content.length(); i++) {
            if (i < 4) {
                Character c = content.charAt(i);
                if (c >= '0' && c <= '9') {
                    rev += c.toString(); 
                }
            }
        }
        return rev;
    }
 

Listing 7 – Numeric Text Field Validation