Kotlin with JavaFX - ListView css class applying also in empty rows

Viewed 210

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.

1 Answers

I'll answer my question and take the opportunity to clarify how I solved it:

As I said, the problem was that I was regenerating the "ObservableList" that contained the customer data, instead of just updating the "active" property.

Earlier in the code, what was done was to use the generateListView() method to remove the items from the ListView and repopulate it again, each time a client was disabled or enabled. To fix it I created the updateListViewContent() method. As you can see, this method only updates the "active" property, taking the value assigned inside 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()
}

listCustomers is a list that contains maps with the data of each customer, and lvCustomers is the ListView that contains the items of type CustomerData. If the id of the client that has been disabled or enabled matches one of those that exists in the ListView, the active property is updated.

The active property can be changed directly by clicking the buttons. This example doesn't really need listCustomers. This is a variable that I use in the original code to store extra data, which does not belong to this ListView, and which is collected in forms presented in a "step-by-step" way.

Related