Dr_Click
Dr_Click

Reputation: 469

Strange behavior of a RadioButton in a TableView

My question is about a strange behavior of a copound in a tableView. The aim is to display an list of players participating to a match in a tableView. The informations displayed are the name of the player, his score, his number of successive busts and an indicator to know if it is his turn to play.

This indicator is a RadioButton as it looks better than a checkBox. When a turn comes to a player, the RadioButton will be setSelected(true), else, it'll be setSelected(false). The true or false information is given by the player's information used in the tableView. Of course, the RadioButton is in "read-only" mode.

Here is my code for the tableView :

TableView<PlayerProgressInformations> playersProgressTable = new TableView<PlayerProgressInformations>();       
defineColumns(playersProgressTable);
playersProgressTable.setItems(playersAtThisTable);

for the defineColumns method :

TableColumn<PlayerProgressInformations, Boolean> colPlaying = new TableColumn<PlayerProgressInformations, Boolean>("Tour");
colPlaying.setPrefWidth(70);
TableCell<PlayerProgressInformations, Boolean>>) new RadioButton());

colPlaying.setCellValueFactory(new Callback<CellDataFeatures<PlayerProgressInformations, Boolean>, ObservableValue<Boolean>>() {
    public ObservableValue<Boolean> call(CellDataFeatures<PlayerProgressInformations, Boolean> p) {
        return new SimpleBooleanProperty(p.getValue().isPlaying());
        }
    });

colPlaying.setCellFactory(new Callback<TableColumn<PlayerProgressInformations, Boolean>, TableCell<PlayerProgressInformations, Boolean>>() {

    @Override
    public TableCell<PlayerProgressInformations, Boolean> call( TableColumn<PlayerProgressInformations, Boolean> param) {
        RadioButtonCell<PlayerProgressInformations, Boolean> radioButtonCell = 
            new RadioButtonCell<PlayerProgressInformations, Boolean>();
        return radioButtonCell;
    }           
});

And the RadioButtoCell class :

private class RadioButtonCell<S, T> extends TableCell<S,T> {
    public RadioButtonCell () {
    }

    @Override
    protected void updateItem (T item, boolean empty) {

        System.out.println("Count value : "+count); //Indicator to check how many times is used the method "updateItem"
        count+=1;
        if (item instanceof Boolean) {
            Boolean myBoolean = (Boolean) item;
            if (!empty) {
                System.out.println("Valeur du boolean : "+item);
                RadioButton radioButton = new RadioButton();
                radioButton.setDisable(true);
                radioButton.setSelected(myBoolean);
                radioButton.setStyle("-fx-opacity: 1");                 
                setGraphic(radioButton);
            }
        }                       
    }
}

The stranges behaviors are the ones bellow :

Could you help me to understand what is happening and how to solve those bugs ?

Thank you very much

Upvotes: 3

Views: 415

Answers (2)

James_D
James_D

Reputation: 209408

Here's another sample that works. (I was almost done with it when @jewelsea posted, so figured I would go ahead and post it anyway. It is similar but the differences between the two might be useful.)

The main issues in your code that I can see are:

  1. Your cell value factory returns a new BooleanProperty every time it is invoked. This means the cell can't observe the correct property, and has no opportunity to update itself when the value changes. You should use JavaFX properties in your model class so that the cell can observe a single property.
  2. Your cell implementation doesn't call the superclass implementation of updateItem(...). This means basic functionality won't happen, so updates may or may not occur when they are needed, and selection won't work, etc.
  3. Your cell implementation doesn't deal with empty cells, so those will not necessarily display correctly.

In this implementation I used a Game class to keep the "current player" and gave each player a reference to the (same) game instance. The playing property in the Player is exposed as a read-only property and its value is bound to the game's current player. This means that only one player can have playing==true, without lots of "wiring" between the players.

The buttons allow for testing adding new players (who are automatically set as the "current" player) or moving the current player to the next player.


import java.util.Iterator;
import java.util.List;

import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class PlayerTable extends Application {

    @Override
    public void start(Stage primaryStage) {
        TableView<Player> table = new TableView<>();

        TableColumn<Player, String> playerCol = new TableColumn<>("Name");
        playerCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));

        TableColumn<Player, Boolean> playingColumn = new TableColumn<>("Playing");
        playingColumn.setCellValueFactory(cellData -> cellData.getValue().playingProperty());
        playingColumn.setCellFactory(tc -> new RadioButtonTableCell<>());

        table.getColumns().add(playerCol);
        table.getColumns().add(playingColumn);

        Game game = new Game();

        Button newPlayerButton = new Button("New Player");
        newPlayerButton.setOnAction(e -> addNewPlayer(game, table.getItems()));

        Button nextPlayerButton = new Button("Next player");
        nextPlayerButton.setOnAction(e -> selectNextPlayer(game, table.getItems()));

        HBox controls = new HBox(5, newPlayerButton, nextPlayerButton);
        controls.setAlignment(Pos.CENTER);
        controls.setPadding(new Insets(5));

        BorderPane root = new BorderPane();
        root.setCenter(table);
        root.setBottom(controls);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void addNewPlayer(Game game, List<Player> players) {
        int playerNumber = players.size() + 1 ;
        Player newPlayer = new Player(game, "Player "+playerNumber);
        game.setCurrentPlayer(newPlayer);
        players.add(newPlayer);
    }

    private void selectNextPlayer(Game game, List<Player> players) {
        if (players.isEmpty()) return ;
        for (Iterator<Player> i = players.iterator() ; i.hasNext() ;) {
            if (i.next() == game.getCurrentPlayer()) {
                if (i.hasNext()) {
                    game.setCurrentPlayer(i.next());
                } else {
                    game.setCurrentPlayer(players.get(0));
                }
                return ;
            }
        }
        game.setCurrentPlayer(players.get(0));
    }

    public static class RadioButtonTableCell<S> extends TableCell<S, Boolean> {

        private RadioButton radioButton ;

        public RadioButtonTableCell() {
            radioButton = new RadioButton();
            radioButton.setDisable(true);
        }

        @Override
        protected void updateItem(Boolean item, boolean empty) {
            super.updateItem(item, empty);

            if (empty) {
                setGraphic(null);
            } else {
                radioButton.setSelected(item);
                setGraphic(radioButton);
            }

        }
    };

    public static class Game {

        private final ObjectProperty<Player> currentPlayer = new SimpleObjectProperty<>() ;

        public ObjectProperty<Player> currentPlayerProperty() {
            return currentPlayer ;
        }

        public Player getCurrentPlayer() {
            return currentPlayerProperty().get();
        }

        public void setCurrentPlayer(Player player) {
            currentPlayerProperty().set(player);
        }
    }

    public static class Player {

        private final String name ;
        private final ReadOnlyBooleanWrapper playing ;

        public Player(Game game, String name) {
            this.name = name ;
            playing = new ReadOnlyBooleanWrapper() ;
            playing.bind(game.currentPlayerProperty().isEqualTo(this));
        }

        public String getName() {
            return name ;
        }

        public ReadOnlyBooleanProperty playingProperty() {
            return playing.getReadOnlyProperty() ;
        }

        public boolean isPlaying() {
            return playingProperty().get();
        }
    }

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

Upvotes: 3

jewelsea
jewelsea

Reputation: 159416

Sample Code

Here is a table implementation that works. I guess you could compare it to your implementation to see what the differences are. Probably the main "fix" is the updateItem handling of empty and null values.

pic

import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;

public class PlayerViewer extends Application {

    private final ObservableList<Player> data =
        FXCollections.observableArrayList(
            new Player("Jacob", true),
            new Player("Isabella", false),
            new Player("Ethan", true)
        );

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

    @Override
    public void start(Stage stage) {
        TableView<Player> table = new TableView<>(data);
        table.setPrefHeight(130);
        table.setPrefWidth(150);

        TableColumn<Player, String> handleCol = new TableColumn<>("Handle");
        handleCol.setCellValueFactory(new PropertyValueFactory<>("handle"));
        table.getColumns().add(handleCol);

        TableColumn<Player, Boolean> playingCol = new TableColumn<>("Playing");
        playingCol.setCellValueFactory(new PropertyValueFactory<>("playing"));
        playingCol.setCellFactory(param -> new TableCell<>() {
            RadioButton indicator = new RadioButton();

            {
                indicator.setDisable(true);
                indicator.setStyle("-fx-opacity: 1");
                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            }

            @Override
            protected void updateItem(Boolean isPlaying, boolean empty) {
                super.updateItem(isPlaying, empty);

                if (empty || isPlaying == null) {
                    setGraphic(null);
                } else {
                    indicator.setSelected(isPlaying);
                    setGraphic(indicator);
                }
            }
        });
        table.getColumns().add(playingCol);

        stage.setScene(new Scene(table));
        stage.show();
    }

    public static class Player {
        private final SimpleStringProperty handle;
        private final SimpleBooleanProperty playing;

        private Player(String handle, boolean playing) {
            this.handle = new SimpleStringProperty(handle);
            this.playing = new SimpleBooleanProperty(playing);
        }

        public SimpleStringProperty handleProperty() {
            return handle;
        }
        public String getHandle() {
            return handle.get();
        }
        public void setHandle(String handle) {
            this.handle.set(handle);
        }

        public SimpleBooleanProperty playingProperty() {
            return playing;
        }
        public boolean isPlaying() {
            return playing.get();
        }
        public void setPlaying(boolean playing) {
            this.playing.set(playing);
        }
    }
}

Additional comments on your question

In terms "Problem 1", of how many times updateItem is called, that's an internal toolkit thing, your code shouldn't really care about that, it just needs to make sure that whenever it is called, that it does the right thing.

Regarding your "Problem 2", regarding the interaction of multiple views for multiple players, who knows? Impossible to say without further additional code, which would probably end up making this question too broad anyway. If you have a specific question about how to handle the interaction of displays for multiple players you will need to expand and clarify your question (likely as a new question with a mcve).

For your implementation, I would advise restyling the radio button (via CSS), so that it doesn't look like a standard user-selectable radio button (because you have disabled the selection capability and then removed the default disabled opacity setting). Or, you could use a custom indicator control such as the Bulb class from this answer, which might be preferred.

Upvotes: 4

Related Questions