I'm developing a Kotlin application with JavaFX, and I've made a wrapper of a "custom ListView". The operation is more or less the following:
The ListView shows a list of clients that is loaded when the application starts. The user has, among many options, two that are relevant: enable and disable the selected client. Disabled clients are highlighted with a special css style, from an external file.
The problem I'm having is that when I disable a client, not only does the css style apply to this client, but also to one of the extra empty rows, and when I press the button to re-enable the same client, I have to do it twice. (once to unstyle the empty row, once to unstyle the correct row).
The application is very large and takes the data of each client from a database, but I've simplified the code to show it here.
TestApplication.kt:
class TestApplication : Application() {
override fun start(stage: Stage) {
val fxmlLoader = FXMLLoader(TestApplication::class.java.getResource("test-view.fxml"))
val scene = Scene(fxmlLoader.load())
stage.title = "Test application"
stage.scene = scene
scene.stylesheets.add(this::class.java.getResource("custom.css").toExternalForm())
stage.show()
}
}
fun main() {
Application.launch(TestApplication::class.java)
}
File TestController.kt:
class TestController: Initializable {
private lateinit var objListViewWrapper: ListViewWrapper
// This MutableList is sent to a model (not included here) to update the customers database table:
private var listCustomers: MutableList<MutableMap<String, String>> = getCustomers()
@FXML
lateinit var lvCustomers: ListView<CustomerData>
override fun initialize(p0: URL?, p1: ResourceBundle?) {
objListViewWrapper = ListViewWrapper(lvCustomers, "id", "text")
generateListView()
}
private fun generateListView(){
objListViewWrapper.clearListView()
objListViewWrapper.populateListView(listCustomers)
}
private fun updateListViewContent(){
listCustomers.forEach { customer ->
val id = customer.getValue("id")
val active = customer.getValue("active").toInt()
lvCustomers.items.forEach { lvCustomer ->
if(lvCustomer.getId() == id){
lvCustomer.setActive(active)
}
}
}
objListViewWrapper.clearSelection()
}
private fun getCustomers(): MutableList<MutableMap<String, String>>{
var li: MutableList<MutableMap<String, String>> = mutableListOf()
val listNames = listOf(
"John Doe", "Jan Jansen",
"Osman Whitfield", "Jane Smith",
"Joe Bloggs", "Michalina Cottrell"
)
listNames.forEachIndexed { index, name ->
val mp: MutableMap<String, String> = mutableMapOf()
mp["id"] = (1 + index).toString()
mp["text"] = name.uppercase()
mp["active"] = "1"
li.add(mp)
}
return li
}
@FXML
fun disableCustomer(){
val selectedItem = objListViewWrapper.getSelectedItem()
if(selectedItem == null){
println("Error: You must select a customer.")
return
}
val idCustomer = selectedItem.getId()
listCustomers.forEach { mutableMap ->
if(idCustomer == mutableMap.getValue("id")){
mutableMap["active"] = "0"
}
}
updateListViewContent()
}
@FXML
fun enableCustomer(){
val selectedItem = objListViewWrapper.getSelectedItem()
if(selectedItem == null){
println("Error: You must select a customer.")
return
}
val idCustomer = selectedItem.getId()
listCustomers.forEach { mutableMap ->
if(idCustomer == mutableMap.getValue("id")){
mutableMap["active"] = "1"
}
}
updateListViewContent()
}
}
File ListViewWrapper.kt:
class CustomerData(id: String, text: String, active: Int) {
var id: SimpleStringProperty = SimpleStringProperty(id)
var text: SimpleStringProperty = SimpleStringProperty(text)
var active: SimpleIntegerProperty = SimpleIntegerProperty(active)
fun getId(): String {
return this.id.get()
}
fun setId(id: String) {
this.id.set(id)
}
fun idProperty(): SimpleStringProperty {
return id
}
fun getText(): String {
return this.text.get()
}
fun setText(text: String) {
this.text.set(text)
}
fun textProperty(): SimpleStringProperty {
return text
}
fun getActive(): Int {
return this.active.get()
}
fun setActive(active: Int) {
this.active.set(active)
}
fun activeProperty(): SimpleIntegerProperty {
return active
}
}
class ListViewWrapper(listView: ListView<CustomerData>, idField: String = "id", textField: String = "text") {
private var listView = listView
private var idField = idField
private var textField = textField
private var obsList: ObservableList<CustomerData> = FXCollections.observableArrayList()
private val cssDisabledRow = "row-disabled"
init {
listView.setCellFactory { lv ->
object : ListCell<CustomerData?>() {
override fun updateItem(obj: CustomerData?, empty: Boolean) {
super.updateItem(obj, empty)
if (empty || obj == null) {
text = null
style = null
graphic = null
} else if(obj.getActive() == 0) {
text = obj.getText()
if(!styleClass.contains(cssDisabledRow)){
styleClass.add(cssDisabledRow)
}
} else {
text = obj.getText()
if(styleClass.contains(cssDisabledRow)){
styleClass.remove(cssDisabledRow)
}
}
}
}
}
}
fun clearListView(){
obsList.clear()
}
fun clearSelection(){
listView.selectionModel.clearSelection()
}
fun populateListView(li: MutableList<MutableMap<String, String>>){
if(li.isEmpty()){
println("The list of items is empty.")
return
}
li.forEach {
val id = it.getValue(idField)
val text = it.getValue(textField)
val active = it.getValue("active").toInt()
obsList.add(CustomerData(id, text, active))
}
listView.items.setAll(obsList)
}
fun getSelectedItem(): CustomerData? {
return listView.selectionModel.selectedItem
}
}
File test-view.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="350.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.testproject.TestController">
<padding>
<Insets bottom="4.0" left="4.0" right="4.0" top="4.0" />
</padding>
<children>
<ListView fx:id="lvCustomers" layoutX="48.0" layoutY="42.0" prefHeight="200.0" prefWidth="200.0" AnchorPane.bottomAnchor="36.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
<HBox alignment="CENTER" layoutX="14.0" layoutY="250.0" prefHeight="32.0" spacing="4.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
<children>
<Button fx:id="btnDisable" mnemonicParsing="false" onAction="#disableCustomer" prefWidth="100.0" text="Disable" />
<Button fx:id="btnEnable" mnemonicParsing="false" onAction="#enableCustomer" prefWidth="100.0" text="Enable" />
</children>
</HBox>
</children>
</AnchorPane>
File custom.css:
.list-cell:filled:hover {
-fx-background-color: #0093ff;
-fx-text-fill: white;
}
.row-disabled{
-fx-background-color: #c2c2c2 !important;
-fx-text-fill: red;
}
.list-cell:filled:selected:focused, .list-cell:filled:selected {
-fx-background-color: linear-gradient(#328BDB 0%, #207BCF 25%, #1973C9 75%, #0A65BF 100%);
-fx-text-fill: white;
}
Screenshots:
disabled rows in dark gray so that they are more noticeable here
EDIT:
Thanks to jewelsea's comments I've realized how to work with the ListView. The ListView didn't need to be regenerated, it just had to update the "active" property. I've edited the code with the modifications to make it work. I know the code is long, and can be cumbersome for those trying to find the problem, but I think this will clear up any doubts newbies have.