Main Scene

With the database loaded, we can now build the main UI part of our app. The Main Scene will have a search area on the top, and below we'll display the results of a search. The results will be displayed inside a ListView control, which is a scrollable list of items.

ListView displays its items as ListCell controls. ListCell is a generic type, where the generic parameter represents the item ListView is rendering. In our case, this is the DictionaryEntry type. Since we want to tell ListView how to render our DictionaryEntry type, we'll create our own DictionaryEntryCell type that extends ListCell and overrides its updateItem method. First, we'll add a new FXML file to the resources folder. dictionary_entry_cell.fxml represents the UI part of our custom ListCell:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>

<VBox fx:id="cellHost" maxHeight="Infinity" minWidth="40" prefWidth="100.0"
xmlns="http://javafx.com/javafx/8.0.121"
xmlns:fx="http://javafx.com/fxml/1">
<VBox.margin>
<Insets top="10.0"/>
</VBox.margin>

<Label fx:id="termLbl" lineSpacing="3.0">
<VBox.margin>
<Insets top="5.0"/>
</VBox.margin>
<font>
<Font name="System Bold" size="22.0"/>
</font>
</Label>

<Label fx:id="explanationLbl" lineSpacing="1.0" minWidth="200.0"
text="Definition" textAlignment="JUSTIFY" textFill="#6f6f6f">
<VBox.margin>
<Insets left="8.0" top="8.0"/>
</VBox.margin>
<font>
<Font name="Arial" size="12.0"/>
</font>
</Label>
<padding>
<Insets bottom="4.0" left="16.0" right="12.0" top="8.0"/>
</padding>

</VBox>

The cell has two label controls, stacked one above the other inside a VBox. Notice how these controls have the fx:id attribute. This allows us to reference these controls inside a controller. When JavaFx instantiates a controller, it will scan all fields (or properties in Kotlin) that have the FXML annotation, and if the field type matches the JavaFx Control type and the name equals the fx:id attribute, it will assign that control to that field. This will be needed, because we'll be setting the text property of these labels dynamically, based on the DictionaryEntry object's data.

Now, we can create a class that will load this FXML file:

class DictionaryEntryCell: ListCell<DictionaryEntry>() {

@FXML
private lateinit var termLbl: Label

@FXML
private lateinit var explanationLbl: Label

@FXML
private lateinit var cellHost: VBox

init {
val fxmlLoader = FXMLLoader(javaClass.classLoader.getResource("dictionary_entry_cell.fxml"))
fxmlLoader.setController(this)
fxmlLoader.load<DictionaryEntryCell>()
}

@FXML
fun initialize() {
explanationLbl.isWrapText = true
explanationLbl.textAlignment = TextAlignment.JUSTIFY
}

override fun updateItem(dictionaryEntry: DictionaryEntry?, empty: Boolean) {
super.updateItem(item, empty)
if (dictionaryEntry != null && !empty) {
termLbl.text = dictionaryEntry.term
explanationLbl.text = dictionaryEntry.explanation
graphic = cellHost
} else {
graphic = null
text = ""
}
}
}

You can see how the FXML annotated properties have the same name as the fx:id attribute in the FXML file. They are lateinit because they will be set after the class constructor gets called. The initialize method will be called after all FXML properties are set, so it is safe to access them inside this method.

In the class constructor, we load our FXML file. Controllers can also be set programmatically, as you can see, with the setController method call.

The most important thing is the updateItem method; it gets called by the ListView when it wants to render our DictionaryEntry. It is also called when an item gets removed, so before setting the text values on the labels, we check that the entry is not empty.

Now, we can finally start with the main scene of our app. First, we'll add the main.fxml file to the resources folder:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.Cursor?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>

<VBox fx:id="root" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.121"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.packt.kotlinquickstart.dictionary.MainController">
<HBox alignment="TOP_CENTER" maxHeight="75.0" minHeight="50.0"
prefHeight="50.0" prefWidth="200.0" StackPane.alignment="TOP_CENTER">
<TextArea fx:id="searchText" maxHeight="30.0" minHeight="20.0"
prefHeight="20.0" prefWidth="200.0" promptText="sarch term">
<HBox.margin>
<Insets bottom="10.0" right="20.0" top="10.0"/>
</HBox.margin>
<cursor>
<Cursor fx:constant="TEXT"/>
</cursor>
</TextArea>
<Button id="searchBtn" fx:id="searchBtn" alignment="BASELINE_CENTER"
contentDisplay="CENTER" minHeight="20.0" mnemonicParsing="false"
prefHeight="30.0" text="Search">
<HBox.margin>
<Insets top="10.0"/>
</HBox.margin>
</Button>
<CheckBox fx:id="exactMatchCb" text="Exact match">
<HBox.margin>
<Insets left="40.0" top="15.0"/>
</HBox.margin>
</CheckBox>
</HBox>
<Label fx:id="noResultsText" managed="false" maxHeight="-Infinity"
prefHeight="30.0" text="No results matching the search"
VBox.vgrow="NEVER">
<VBox.margin>
<Insets bottom="10.0" left="20.0" top="10.0"/>
</VBox.margin>
</Label>
</VBox>

Again, nothing special about the UI part. We use VBoxes for vertical stacking and HBoxes for the horizontal stacking of child controls. We haven't defined the ListView control in the FXML; we'll be creating it in Kotlin code. There were some problems with clearing items from the ListView; sometimes, when setting new search results and new items for the ListView, it displayed items from the previous search results. To avoid this, each search will get a new instance of the ListView class.

The FXML file already has the fx:controller attribute; now we'll create this final class of our app. The MainController class will be responsible for taking the search input and querying the database for results. First, let's add all the FXML controls we defined in the FXML file:

class MainController {

@FXML
private lateinit var root: VBox

@FXML
private lateinit var searchBtn: Button

@FXML
private lateinit var searchText: TextArea

@FXML
private lateinit var exactMatchCb: CheckBox

@FXML
private lateinit var noResultsText: Label

private var listView: ListView<DictionaryEntry>? = null

The ListView property doesn't have the FXML annotation, as it is not defined in FXML. Other properties will be set by JavaFx, when it creates an instance of this controller class.

We'll also need a Repository instance and an instance of a generic ObservableList interface:

private val repository = Repository()
private val observableItems = FXCollections.observableArrayList<DictionaryEntry>()

The ListView constructor accepts an ObservableList object and this is the one we'll use for storing our search results.

We'll also need two helper functions, which will be used for recreating the Listview:

private fun clearListView() 
{
root.children.removeAll(listView)
observableItems.clear()
createListView()
}

private fun createListView()
{
listView = ListView(observableItems)
listView.cellFactory = Callback { DictionaryEntryCell() }
root.children.add(listView)
VBox.setVgrow(listView, Priority.ALWAYS)
}

Triggering a search can be done with a click on the search button or by pressing the Enter key in the text area. In both cases, we'll respond to the appropriate event so we need to define event handlers for these two controls:

@FXML
fun initialize()
{
searchBtn.onMouseClicked = EventHandler
{

onSearchClicked()

}

searchText.onKeyPressed = EventHandler { event ->
if (event.code == KeyCode.ENTER) {
event.consume()
onSearchClicked()
}
}

createListView()
}

Finally, here is the onSearchClicked function:

private fun onSearchClicked() 
{
noResultsText.isManaged = false
clearListView()

val text = searchText.text?.trim()
if (text != null && text.isNotEmpty())
{
CompletableFuture.supplyAsync
{
repository.search(text, exactMatchCb.isSelected)
}.thenAcceptAsync(Consumer { results ->
if (results.isNotEmpty())
{
observableItems.addAll(results)
} else {
noResultsText.isManaged = true
}
}, MainThreadExecutor.INSTANCE)
}
}

Before querying the database, we hide the noResultsText control and recreate listView. Since we are interested in the result of CompletableFuture, we create it with the supplyAsync call. This enables us to get the results with the thenAcceptAsync function call, which is scheduled on the main thread because it will interact with our UI controls. When we have the results, we add them to the observableItems object. Since ObservableList knows when items change, ListView will render the new items automatically. And if the results are empty, we just show noResultsText.

We can now run the app, and if the database has been loaded, we'll enter the main screen of our app where we can search for words and definitions:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.219.80