Module 3 Task 2: Adding a Text Control


Launch JavaFX Media Browser Application Download NetBeans Project

Introduction

This task adds a title bar at the top of the wall and positions a search text box within it. The search function results populate the wall.

Running the Project

  1. Download the Module 3 Task 2 NetBeans project.

  2. Run the project.

  3. Click in the search text box. Type a search string and press Enter. Notice that the opacity of the default search text changes as the mouse enters the text box.

Wall with search text box Figure 1

Architecture

This task adds the custom nodes TitleBar.fx and SearchTextBox.fx, as shown in Figure 2.

  • TitleBar is a container for the service name (a text string). In this task the service name is Yahoo!

  • SearchTextBox uses javafx.scene.control.TextBox and uses the WebSearch API, invoking a new search when the user enters a search term.

  • Wall.fx displays the title bar and the search text box.

Architecture for Module 3 Task 2 Figure 2

Creating the Title Bar

TitleBar is a CustomNode containing the title, a facade, and a border. The content will eventually be initialized in Wall.fx.

Source Code
    override function create(): Node {
        return
        Group {
            content: [
                facade,
                border,
                title
            ]
        };
    }

Creating the Title Text

TitleBar uses javafx.scene.text.Text to display the serviceName, which identifies the web service. In this case the serviceName is Yahoo!. The serviceName is more than a label. The application maintains a serviceName to support future functionality where multiple web services APIs might be supported and multiple service names might exist.

Source Code
    def title : Text = Text {
        font : titleFont
        strokeWidth: 2
        fill: Color.rgb(176, 198, 255)

        translateX: bind (stageWidth - title.boundsInLocal.width) / 2

        translateY: bind (facade.boundsInLocal.height 
                          - title.boundsInLocal.height)/2 + titleFont.size

        content: serviceName
        
    }

Notice that there is no need to bind the Text content to serviceName. The serviceName is initialized in Wall.fx, as discussed in Adding the Title Bar and the Search Text Box to Wall.fx, and does not change. Also notice that the size of the font needs to be taken into consideration when centering the text vertically. Recall that the default origin for Text is TextOrigin.BASELINE so the additional offset of the font size is needed. Centering Text with an origin of TOP or BOTTOM requires an extra calculation to compensate for the height of the descender.

The titleFont is defined in TitleBar.fx, but outside the TitleBar class. This is a script-level variable, meaning only one instance of titleFont will be created. Because the definition of titleFont does not change, there is no reason for it to be an instance variable.

The fill color matches the color of the SearchTextBox border.

Creating a Linear Gradient and Using It In the Facade

The linear gradient definition is assigned to the variable slate. The stops are points where the color values change (see javafx.scene.paint.LinearGradient.html in the API documentation). The constant SCROLLCTL_COLOR was originally defined in Module 2 so it is being reused with different stops. Like titleFont, slate is also defined as a script-level variable since its definition does not change.

Source Code
 def slate = LinearGradient{
        startX: 0
        startY: 0
        endX: 0
        endY: 1
        proportional: true
        stops: [
            Stop { offset: 0.0 color: Constants.SCROLLCTL_COLOR },
            Stop { offset: 0.5 color: Color.BLACK }
            Stop { offset: 1.0 color: Constants.SCROLLCTL_COLOR },
        ]
    }

The facade is a rectangle filled with slate, the linear gradient.

Source Code
    def facade : Rectangle = Rectangle {
        translateX: 0
        translateY: 0
        x: 0
        y: 0
        width: bind stageWidth
        height: Constants.TITLE_BAR_FACADE_HEIGHT
        opacity: 0.3
        fill: slate
    } 

Note that opacity is used to lighten the appearance of slate without changing its definition.

Drawing a Horizontal Line

The border consists of lighter colored lines positioned at the top and bottom of the facade. In the code below, javafx.scene.shape.Path is used to draw the lines.

Source Code
    def border : Path = Path {
        opacity: 0.3
        stroke: Constants.SCROLLCTL_COLOR
        strokeWidth: 2
        elements: [
            MoveTo{ x: 0, y: 0 },
            HLineTo{ x: bind stageWidth },
            MoveTo{ x: 0, y: Constants.TITLE_BAR_FACADE_HEIGHT },
            HLineTo{ x: bind stageWidth }
        ]
    }

The positioning is relative to the stage. Think of Path as drawing with a pencil. MoveTo is like picking up the pencil and setting it down at the given point. HLineTo actually draws the line (horizontally in this case) from the given point to the given end point. Note that the end points of the lines are bound to stageWidth so the lines are redrawn if the width of the stage changes.

javafx.scene.shape.LineTo could also be used instead of Path. Had the code been written this way, there would have been one LineTo for the top line and another for the bottom.

Creating the Search Text Box

The SearchTextBox uses javafx.lang.control.TextBox to accept user input and uses WebSearch to initiate a new search.

Source Code
    def searchTB : TextBox = TextBox {
        columns : 15
        visible : true
        width : Constants.SEARCH_BOX_WIDTH
        height : Constants.SEARCH_BOX_HEIGHT
        focusable : true
        selectOnFocus : true
        
        action : function() {
            searchTB.focusable = false;
            getMetaDataforSearchText();
        }

        opacity: .25

        onMouseEntered: function(ev : MouseEvent) : Void {
            if ( not searchTB.focused ) {
                searchTB.opacity = .75;
            }
        }

        onMouseExited: function(ev : MouseEvent) : Void {
            if ( not searchTB.focused ) {
                searchTB.opacity = .25;
            }
        }
    }

Note the action function. This is the function that is called by TextBox when an action is fired, typically in response to KeyCode.VK_ENTER. The reason for setting focusable to false is explained later.

This class also has onMouseEntered and onMouseExited actions that change the box opacity, provided the searchTB does not have focus. If the the searchTB does have focus, the opacity does not change on mouse over. This effectively provides a "click here" affordance.

Notice also that the appearance of the TextBox is not specified here; yet, among other things, the border is a certain color and the corners have a certain radius. The appearance of the TextBox is controlled by applying a stylesheet. In the project's src/mediabrowser directory there is a file named TextBoxSkin.css. This file has CSS styles for when the TextBox has focus, and for when it does not. Using these styles requires the addition of a stylesheet to the scene in Main.fx:

Source Code
        stylesheets: [
            MediaComponent.css_skins,
            Constants.TEXTBOX_CSS_SKIN
        ]

When the user types a search query and presses Enter, the getMetaDataforSearchText function is called:

Source Code
    function getMetaDataforSearchText() : Void {
        if ( clearSearchResults != null ) {
             clearSearchResults();
        }
        webSearch.searchQuery = searchTB.value.trim();
        webSearch.clearSearchResults();
        webSearch.search();
    }

The SearchTextBox uses the WebSearch API directly. TextBox.value is the committed value of the text; that is, the value after the user hits Enter (typically). The function clearSearchResults is invoked, if it is defined. This optional function allows Wall to prepare for new search results. Likewise, WebSearch.clearSearchResults notifies the WebSearch instance to make way for a new search, which it does by emptying the WebSearch results sequence, causing the thumbnails to be removed from the Wall.

In SearchTextBox action, a call is made to set searchTB.focusable to false. This causes the SearchTextBox to lose focus when the user presses Enter, allowing the Wall to regain focus for keyboard navigation. The SearchTextBox needs to be focusable, however, in order to receive keyboard input. The following block of code resets focusable to true when searchTB.focused becomes false (as happens when focusable is set to false in the action function). Refer to the comments in the code for further details.

Source Code
    override var focused = bind searchTB.focused on replace {
        if ( not focused ) {
            searchTB.opacity = .25;
            searchTB.focusable = true;
            searchTB.value = webSearch.searchQuery
        } else {
            searchTB.opacity = .75

        }
    }

Adding the Title Bar and the Search Text Box to Wall.fx

Over in Wall.fx the title bar and the search text box are added to the Wall

Source Code
    def wall : Group = Group {
        content: [
            titleBar,
            searchTB,
            thumbs,
            scrollCtl
        ]

The titleBar spans the width of the stage and serviceName is initialized to the WebSearch instance's serviceName. Note that there is no need to bind serviceName since webSearch.serviceName will not change after initialization.

Source Code
    def titleBar : TitleBar = TitleBar {
        stageWidth: bind maxVisibleWidth;
        serviceName : webSearch.serviceName
    };

The SearchTextBox is centered on the TitleBar and offset from the right edge by Constants.BORDER_WIDTH. Note here the implementation of clearSearchResults(). This is the function that is called from the SearchTextBox just before a new search is enqueued. Since the thumbnails sequence is about to be emptied (from the call to webSearch.clearSearchResults() in SearchTextBox), the Wall resets the thumbnail with focus:

Source Code
    def searchTB :  SearchTextBox = SearchTextBox {
        translateX : bind maxVisibleWidth - searchTB.boundsInLocal.width 
                                          - Constants.BORDER_WIDTH
        translateY : bind (titleBar.boundsInLocal.height 
                           - searchTB.boundsInLocal.height)/2
        webSearch: webSearch
        clearSearchResults: function () : Void {
            thumbnails[thumbWithFocus].hasFocus = false;
            thumbWithFocus = 0;
        }
    };

Recall that the SearchTextBox is forced to give up focus when the user presses Enter. This allows the Wall to regain focus for keyboard navigation. Wall watches for changes the SearchTextBox's focus state with the variable searchBoxLostFocus, which is bound to the inverse of searchTB.focused. If the search text box loses focus, the following code triggers Wall to request focus.

Source Code
    def searchBoxLostFocus = bind not searchTB.focused on replace {
        if ( searchBoxLostFocus ) {
            wall.requestFocus();
        }
    };

Try It

  • Try entering different search strings.

  • In TitleBar.fx, try drawing some lines using LineTo (the line can be diagonal).

  • Try changing the appearance of the SearchTextBox by modifying the styles in TextBoxSkin.css.

  • What happens if you don't call clearSearchResults from SearchTextBox on the WebSearch instance?

  • What happens if you comment out the "on replace" block in either SearchTextBox.focused or Wall.searchBoxLostFocus?

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