Déjà vu
Déjà vu

Reputation: 783

JavaFX combobox, on item clicked

My problem is as follows,

For the sake of this question I reproduced the problem in a new project.

Say I have this application with a combobox in it, there could be 1 or more items in there. And I would like it to be so that when the user clicks an item in the combobox that 'something' happens.

I produced the following code:

        obsvList.add("item1");

        cbTest.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                System.out.println("Item clicked");
            }
        });

This works when the application starts and an item is selected for the first time. This also works when there are 2 or more items in the combobox (when the user clicks item 1, then item 2, then item 1 for example)

However my problem is that when there is only 1 item in the combobox, let's say "item1". And the user reopens the combobox and clicks "item1" again then it won't redo the action.

enter image description here

It will only print the line "Item Clicked" when a 'new' item is clicked.

I hope it made it clear what the problem i'm experiencing is, if not please ask for clarification and I will give so where needed.

Thanks in advance!

Upvotes: 1

Views: 15736

Answers (2)

javasuns
javasuns

Reputation: 1111

The idea is to set a listener on the ListView pane, that appears whenever you click on the ComboBox. The ListView instance is created once the ComboBox is first loaded in the JavaFX scene. Therefore, we add a listener on the ComboBox to check when it appears on the scene, and then through the "lookup" method we get the ListView and add a listener to it.

private EventHandler<MouseEvent> cboxMouseEventHandler;

private void initComboBox() {
    ComboBox<String> comboBox = new ComboBox<String>();
    comboBox.getItems().add("Item 1");
    comboBox.getItems().add("Item 2");
    comboBox.getItems().add("Item 3");

    comboBox.sceneProperty().addListener((a,oldScene,newScene) -> {
        if(newScene == null || cboxMouseEventHandler != null)
            return;
            
        ListView<?> listView = (ListView<?>) comboBox.lookup(".list-view");
        if(listView != null) {
            cboxMouseEventHandler = (e) -> {
                Platform.runLater(()-> {
                    String selectedValue = (String) listView.getSelectionModel().getSelectedItem();
                    if(selectedValue.equals("Item 1"))
                        System.out.println("Item 1 clicked");
                });
            }; // cboxMouseEventHandler
        
            listView.addEventFilter(MouseEvent.MOUSE_PRESSED, cboxMouseEventHandler); 
        } // if
    });
} // initComboBox

Upvotes: 0

James_D
James_D

Reputation: 209245

The functionality of a combo box is to present the user with a list of options from which to choose. When you are using a control which implies selection, you should really ensure that the UI is always consistent with the option that is selected. If you do this, then it makes no sense to "repeat an action" when the user "reselects" the same option (because the UI is already in the required state). One approach to this is to use binding or listeners on the combo box's value:

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class ComboBoxExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        ComboBox<Item> choices = new ComboBox<>();
        for (int i = 1 ; i <=3 ; i++) {
            choices.getItems().add(new Item("Choice "+i, "These are the details for choice "+i));
        }

        Label label = new Label();

        choices.valueProperty().addListener((obs, oldItem, newItem) -> {
            label.textProperty().unbind();
            if (newItem == null) {
                label.setText("");
            } else {
                label.textProperty().bind(newItem.detailsProperty());
            }
        });

        BorderPane root = new BorderPane();
        root.setCenter(label);
        root.setTop(choices);

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

    }

    public class Item {
        private final String name ;
        private final StringProperty details = new SimpleStringProperty() ;

        public Item(String name, String details) {
            this.name = name ;
            setDetails(details) ;
        }

        public String getName() {
            return name ;
        }

        @Override
        public String toString() {
            return getName();
        }

        public final StringProperty detailsProperty() {
            return this.details;
        }


        public final String getDetails() {
            return this.detailsProperty().get();
        }


        public final void setDetails(final String details) {
            this.detailsProperty().set(details);
        }



    }

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

In this case, there is never a need to repeat an action when the user "reselects" the same option, because the code always assures that the UI is consistent with what is selected anyway (there is necessarily nothing to do if the user selects the option that is already selected). By using bindings in the part of the UI showing the details (just a simple label in this case), we are assured that the UI stays up to date if the data changes externally. (Obviously in a real application, this may be far more complex, but the basic strategy is still exactly the same.)

On the other hand, functionality that requires an action to be repeated if the user selects the same functionality is better considered as presenting the user with a set of "actions". The appropriate controls for this are things like menus, toolbars with buttons, and MenuButtons.

An example of a set of repeatable actions is:

import java.util.stream.Collectors;
import java.util.stream.Stream;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class MenuButtonExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        MenuButton menuButton = new MenuButton("Items");
        Label label = new Label();

        Item[] items = new Item[3];
        for (int i = 1 ; i <=3 ; i++) {
            items[i-1] = new Item("Item "+i);
        }

        for (Item item : items) {
            MenuItem menuItem = new MenuItem(item.getName());
            menuItem.setOnAction(e -> item.setTimesChosen(item.getTimesChosen() + 1));
            menuButton.getItems().add(menuItem);
        }

        label.textProperty().bind(Bindings.createStringBinding(() -> 
            Stream.of(items)
                .map(item -> String.format("%s chosen %d times", item.getName(), item.getTimesChosen()))
                .collect(Collectors.joining("\n")), 
            Stream.of(items)
                .map(Item::timesChosenProperty)
                .collect(Collectors.toList()).toArray(new IntegerProperty[0])));

        BorderPane root = new BorderPane();
        root.setCenter(label);
        root.setTop(menuButton);

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

    public static class Item {
        private final String name ;
        private final IntegerProperty timesChosen = new SimpleIntegerProperty();

        public Item(String name) {
            this.name = name ;
        }

        public String getName() {
            return name ;
        }

        @Override
        public String toString() {
            return getName();
        }

        public final IntegerProperty timesChosenProperty() {
            return this.timesChosen;
        }


        public final int getTimesChosen() {
            return this.timesChosenProperty().get();
        }


        public final void setTimesChosen(final int timesChosen) {
            this.timesChosenProperty().set(timesChosen);
        }



    }

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

Upvotes: 6

Related Questions