How do I activate a toggle button in Tableview if I select a row using JavaFX?

132 Views Asked by At

I have a table. It has a list of voice channels. Each row has a toggle button that can listen or mute the channel. I have two listeners, One for clicking on a row and another for selecting the toggle button. How do I change the button state if I click on a row? (For example, I click on "Voice Channel 1" and the button in that row changes to "Mute", in other words, if I select a channel, I automatically start listening to it).

public class VoiceChannel {
    private final StringProperty voiceChannelName = new SimpleStringProperty();

    public VoiceChannel(String userName) {
        setVoiceChannelName(userName);
    }

    public final StringProperty voiceChannelNameProperty() {
        return this.voiceChannelName;
    }

    public final String getVoiceChannelName() {
        return this.voiceChannelNameProperty().get();
    }

    public final void setVoiceChannelName(String voiceChannelName) {
        this.voiceChannelNameProperty().set(voiceChannelName);
    }
}
public class Main extends Application {
    @Override
    public void start(Stage stage) {

        TableView<VoiceChannel> voiceChannelTable = new TableView<>();
        TableColumn<VoiceChannel, String> voiceChannelNameColumn = new TableColumn<>("Channel Name");
        voiceChannelNameColumn.setCellValueFactory(cellData ->    cellData.getValue().voiceChannelNameProperty());
        TableColumn<VoiceChannel, VoiceChannel> listenMuteColumn = new TableColumn<>("Listen/Mute");
        listenMuteColumn.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue()));

        ObservableSet<VoiceChannel> channels = FXCollections.observableSet();
        channels.add(new VoiceChannel("Voice Channel 1"));
        channels.add(new VoiceChannel("Voice Channel 2"));
        channels.add(new VoiceChannel("Voice Channel 3"));
        channels.add(new VoiceChannel("Voice Channel 4"));
        channels.add(new VoiceChannel("Voice Channel 5"));

        voiceChannelTable.getSelectionModel().selectedItemProperty().addListener((observableValue, oldValue, newValue) -> {
            if(newValue != null) {
                System.out.println(newValue.getVoiceChannelName() + " selected");
            }
        });

        listenMuteColumn.setCellFactory(c -> new TableCell<>() {
            private final ToggleButton button = new ToggleButton();
            {
                button.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
                    if (isNowSelected) {
                        System.out.println("Now listening to " + getItem().getVoiceChannelName());
                    } else {
                        System.out.println(getItem().getVoiceChannelName() + " muted");
                    }
                });
                button.textProperty().bind(Bindings.when(button.selectedProperty()).then("Mute").otherwise("Listen"));
                setAlignment(Pos.CENTER);
            }

            @Override
            public void updateItem(VoiceChannel item, boolean empty) {
                super.updateItem(item, empty);
                if (empty) {
                    setGraphic(null);
                } else {
                    setGraphic(button);
                }
            }
        });

        voiceChannelTable.getColumns().add(voiceChannelNameColumn);
        voiceChannelTable.getColumns().add(listenMuteColumn);

        voiceChannelTable.getItems().addAll(channels);

        Scene scene = new Scene(new BorderPane(voiceChannelTable), 400, 300);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

I can't seem to get the listeners to talk to each other. I saw an example online using Callbacks but I am not familiar with this process.

1

There are 1 best solutions below

0
DaveB On

Here's a little application that has the core of what you're looking for, so that you can see how it would work:

class ChannelPlayer : Application() {
   override fun start(primaryStage: Stage) {
      primaryStage.scene = Scene(createContent())
      primaryStage.show()
   }

   private fun createContent(): Region = BorderPane().apply {
      val channelPlaying: StringProperty = SimpleStringProperty("")
      val channelList = createData()
      channelList.forEach {
         it.isPlaying.addListener { _ ->
            if (it.isPlaying.value) {
               channelPlaying.value = it.channelName.value
               channelList.filtered { channel -> channel != it }.forEach { channel ->
                  channel.isPlaying.value = false
               }
            }
         }
      }
      center = TableView<ChannelData>().apply {
         columns += TableColumn<ChannelData, String>("Channel Name").apply {
            cellValueFactory = Callback { p -> p.value.channelName }
         }
         columns += PlayMuteColumn()
         items = channelList
         selectionModel.selectedItemProperty().addListener { _ ->
            selectionModel.selectedItem.isPlaying.value = true
         }
      }
      bottom = VBox(5.0, Label("Now Playing:"), Label().apply {
         textProperty().bind(channelPlaying)
      })
   }

   private fun createData() = FXCollections.observableArrayList(listOf(ChannelData("Channel 1"),
                                                                       ChannelData("Channel 2"),
                                                                       ChannelData("Channel 3")))
}

class ChannelData(name: String = "") {
   val channelName: StringProperty = SimpleStringProperty(name)
   val isPlaying: BooleanProperty = SimpleBooleanProperty(this, name, false)
}

fun main() = Application.launch(ChannelPlayer::class.java)

and the TableColumn/Cell:

class PlayMuteColumn : TableColumn<ChannelData, BooleanProperty>() {
   init {
      cellFactory = Callback { PlayMuteCell() }
      cellValueFactory = Callback { p -> ReadOnlyObjectWrapper(p.value.isPlaying) }
   }

}

class PlayMuteCell : TableCell<ChannelData, BooleanProperty>() {

   private val toggleButton = ToggleButton().apply {
      textProperty().bind(Bindings.createStringBinding({ if (isSelected) "Playing" else "Muted" }, selectedProperty()))
   }

   override fun updateItem(item: BooleanProperty?, empty: Boolean) {
      itemProperty().value?.run {
         toggleButton.selectedProperty().unbindBidirectional(this)
      }
      super.updateItem(item, empty)
      graphic = if ((item != null) && (item.value != null) && !empty) {
         toggleButton.selectedProperty().bindBidirectional(item)
         toggleButton
      } else {
         null
      }
   }
}

This is Kotlin, but still JavaFX, you should be able to figure out what it's doing.

In this example channelPlaying and channelList constitute the Presentation Model. All of the logic works off this. In the InvalidationListener for each item in channelList it updates the channelPlaying value and flips all of the other channels off. Where it sets the value of channelPlaying is where you would trigger the channel to start playing.

The TableView is just used to present the data to the user. The selectionModel.selectedItemProperty() has a listener on it to flip the isPlaying property in that selectedItem to true. Then the Listener on that property will kick in, play the channel and turn off the others.

Just to keep it clean, I created a separate class for the TableColumn and it pulls the values and creates the cells.

The thing to note here is that the value being passed to the Cell is the BooleanProperty itself. So the itemProperty of the cell contains a BooleanProperty. This means that the value factory has to wrap the BooleanProperty in a ReadOnlyObjectWrapper so it will stay a BooleanProperty and not a Boolean.

In the Cell, there's just a ToggleButton. It's textProperty() is bound to whether it's selected or not.

In PlayMuteCell.updateItem() is the only tricky part. The first step is to unbind the ToggleButton selected property if the cell was previous populated. Then it's bidirectionally bound to the incoming BooleanProperty.

The result is that when you click on the ToggleButton it also flips the BooleanProperty in the ChannelData item which triggers the Listener on it which would turn on/off the playback and do the other stuff.