Module 2 Task 3: Adding Keyboard Navigation


Launch JavaFX Media Browser Application Download NetBeans Project

Introduction

This task introduces keyboard navigation functionality. The keyboard functionality uses the arrow keys to scroll across rows and columns while considering the number of columns and rows in the thumbnail wall. As you navigate to the last thumbnail that is visible on the stage, the row or column scrolls to reveal more thumbnails within the stage boundary. When you reach the end of a column, the focus automatically moves to the first image in the next column. While an active image is enlarged, you can scroll to the next image, which is automatically enlarged when it has the focus. Pressing the Enter key enlarges the selected image and pressing the escape key dismisses the image.

Running the Project

  1. Download the Module 2 Task 3 NetBeans project and open it in NetBeans.

  2. Run the project.

  3. The top left thumbnail has the focus. Use the arrow keys to navigate within the wall of thumbnails.

Figure 1 shows the architecture for this task.

Far right image selected to show how the active image is in the second column from the right. Figure 1

Building the Keyboard Navigation

The primary keyboard navigation functionality is built in the Wall.fx class. It is referenced in Thumbnail.fx and Main.fx.

Adding the Key Action

The key action is added by using the javafx.scene.input.KeyCode and javafx.scene.input.KeyEvent classes. The following code shows the onKeyPressed function that handles the keyboard navigation. In order for the Wall to receive keyboard input, the Wall must call requestFocus(), which is done in the create() function. Without having the input focus, wall does not receive any keyboard events.

Source Code
onKeyPressed: function(evt: KeyEvent): Void {

    scrollCtl.dragging = true;

    if (evt.code == KeyCode.VK_ENTER) {

    if ( isMediaShowing ) {
         hideMedia();
    } else {
         thumbnails[thumbWithFocus].showFullView();
    }

} else if (evt.code == KeyCode.VK_ESCAPE){

    hideMedia();

} else {

    var newThumbIndex = thumbWithFocus;

    if (evt.code == KeyCode.VK_LEFT) {
        newThumbIndex -= Constants.THUMB_ROWS;

    } else if (evt.code == KeyCode.VK_RIGHT) {
        newThumbIndex += Constants.THUMB_ROWS;

    } else if (evt.code == KeyCode.VK_UP) {
         newThumbIndex -= 1;

    } else if (evt.code == KeyCode.VK_DOWN) {
         newThumbIndex += 1;
}

ScrollControl.dragging is used by the ScrollControl to prevent the centering animation from firing on every key event. This allows the user to quickly scroll through the thumbnails by holding down the navigation keys. The dragging flag is reset to false in the onKeyReleased function farther down in the code.

The thumbWithFocus variable is an index into the thumbnails sequence that corresponds to the thumbnail that has the focus. It is initialized to the first thumbnail. The block of code that handles the onKeyPressed event simply calculates a new value for thumbWithFocus, then scrolls the wall to ensure the thumbnail is visible.

Scrolling the Wall

If the selected thumbnail image is in the second from last column of the stage, you need to make the wall scroll so more columns are visible. The image below shows the active image in the second column from the left side of the stage. If you want to continue scrolling right (Figure 2), you need the application to scroll from left to right so more images are revealed.

Application showing the selected image with a white box surrounding it. Figure 2

The following code sample is the code that ensures the thumbnail with focus is visible. Notice the use of getBoundsInScene() to determine whether or not the thumbnail is visible. The getBoundsInScene() function is relative to the scene in which a Node appears. If getBoundsInScene().maxX is less than zero, then the thumbnail is off the left edge of the stage. If getBoundsInScene().minX is greater than the scene width, then the thumbnail is off the right edge of the stage. The boundsInLocal variable is not much use here since its values are relative to the Wall, not to the scene in which Wall is inserted.

If the thumbnail is not visible, this code block calculates the scrollPosition needed for the the thumbnail to be visible. For more detail, refer to the code in the NetBeans project. The code there is heavily commented .

Source Code
if ( newThumbWithFocus.getBoundsInScene().minX !=
     oldThumbWithFocus.getBoundsInScene().minX ) {

    if (newThumbWithFocus.getBoundsInScene().maxX < 2 * columnWidth) {

        var offset = maxVisibleWidth - columnWidth -
                     newThumbWithFocus.getBoundsInScene().maxX;
        var newScrollPosition = (-scrollOffset - offset)/(thumbsGroupWidth - maxVisibleWidth);

        if (newScrollPosition < 0.0) {
            newScrollPosition = 0.0;
        }
        scrollPosition = newScrollPosition;
    } else if (newThumbWithFocus.getBoundsInScene().minX > maxVisibleWidth - 2 * columnWidth) {
        var offset = newThumbWithFocus.getBoundsInScene().minX - columnWidth;
        var newScrollPosition = (-scrollOffset + offset)/(thumbsGroupWidth - maxVisibleWidth);

        if (newScrollPosition > 1.0) {
            newScrollPosition = 1.0;
        }
        scrollPosition = newScrollPosition;
    }
}

Adding the Selected Image Border

The active image is highlighted by a white border that delineates it from the other images. The code for the border, which is a simple rectangle, is in Thumbnail.fx. By setting thumbnail.hasFocus = true, the border is painted. Look for places in Wall.fx where this is done.

Source Code
def focusOutline : Rectangle =  Rectangle {
        x: bind imageView.boundsInParent.minX - 1
        y: bind imageView.boundsInParent.minY - 1
        width: bind imageView.boundsInParent.width + 2
        height: bind imageView.boundsInParent.height + 2
        strokeWidth: 2
        stroke: Color.WHITE
        fill: null
        visible: bind hasFocus
    };

Changing the Selected Image While Navigating

As thumbWithFocus changes, the selected image (if shown) should change as well. Recall the showMedia functions in Main will show media for the selected thumbnail. To change the selected image, then, should be a simple matter of calling showMedia. But this function is called from Thumbnail. Further, Thumbnail passes data that is not available outside Thumbnail. So the following hook was added Thumbnail to cause it to call showMedia.

Source Code
  package function showFullView() : Void {
        fullView(metaData, image);

Whether or not this function is called is predicated on whether or not there is media currently shown. Up until now, only Main was aware of this. Now this information needs to be available outside Main. The variable isMediaShowing was added to Wall. Its value is initialized in Main as:

isMediaShowing: bind (media != null )

This simply states that the value of isMediaShowing is determined by whether or not media is null. This works since media is set to null in Main.hide.

Try It

When the thumbWithFocus is in the first or last column, navigating left or right (respectively) does not wrap. Fix the logic in the such that the thumbWithFocus wraps from the end of a column to the beginning of the next row, and vice-versa.

The handling of the VK_ENTER and VK_ESCAPE are not really needed in Wall. Try handling VK_ENTER in Thumbnail and VK_ESCAPE in Media.

Also, Thumbnail could call Main's replaceMedia function when the thumbnail gets focus. Main.replaceMedia would need to be tweaked such that it would take no action if media is null. Then the isMediaShowing variable could be eliminated from Wall.


Rate This Article
Discussion

We welcome your participation in our community. Please keep your comments civil and on point. You may optionally provide your email address to be notified of replies—your information is not used for any other purpose. By submitting a comment, you agree to these Terms of Use.

 

English
日本語
한국어
简体中文
русский
Português do Brasil