Module 3 Task 2: Adding a Text Control
- Highlights
-
- Create a search text box
- Use a gradient effect and opacity
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
- Download the Module 3 Task 2 NetBeans project.
- Run the project.
- 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.
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.TextBoxand uses the WebSearch API, invoking a new search when the user enters a search term. Wall.fxdisplays the title bar and the search text box.
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.
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.
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.
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.
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.
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.
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:
stylesheets: [
MediaComponent.css_skins,
Constants.TEXTBOX_CSS_SKIN
]
When the user types a search query and presses Enter, the getMetaDataforSearchText function is called:
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.
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
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.
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:
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.
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.focusedorWall.searchBoxLostFocus?
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 repliesyour information is not used for any other purpose. By submitting a comment, you agree to these Terms of Use.
