Jim D
Jim D

Reputation: 316

Ignoring an event while handling an event

I have a Combobox that displays a list of folders in a directory. There is an onAction Event handler that handles when the user changes the selected item. When changing the selected folder, the items in the list are modified to reflect the folders that contained within the new directory.

The issue I have is that modifying the list of items causes the action event to fire, resulting in a loop.

The two solutions I can think of are while in the event handler temporarily removing the event handler or using an atomic value as a semaphore.

Is there a better approach?

Upvotes: 0

Views: 70

Answers (2)

James_D
James_D

Reputation: 209674

While it seems like the right choice, a ComboBox might not be the best control to use here. The problem is that a ComboBox essentially represents a way for the user to select an item from a list. The selection is persistent until the user chooses a different item, and this latter part doesn't really fit your use case. In your use case, when the user "selects" something, the list immediately changes, so the selected item no longer appears in the list.

Using a control which exposes actions for each item, instead of selection may be a better fit here. The MenuButton control has a button which opens up a menu; you can arrange for the list of menu items to be a list of the subdirectories of the current directory, and for the text of the button to be the name of the current directory.

Here's a prototype of the idea:

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;

public class DirectorySelector {

    private final ObjectProperty<Path> directory = new SimpleObjectProperty<>();

    public ObjectProperty<Path> directoryProperty() {
        return directory;
    }

    public final Path getDirectory() {
        return directoryProperty().get();
    }

    public final void setDirectory(Path directory) {
        if ( ! Files.isDirectory(directory)) {
            throw new IllegalArgumentException(directory + " is not a directory");
        }
        directoryProperty().set(directory);
    }

    private final MenuButton menuButton;

    public DirectorySelector(Path initialDirectory) {
        menuButton = new MenuButton();
        menuButton.textProperty().bind(directory.map(Path::getFileName).map(Path::toString));
        directory.addListener((_, _, newDirectory) -> {
            menuButton.getItems().clear();
            if (newDirectory != null) {
                try {
                    MenuItem up = new MenuItem("..");
                    up.setOnAction(_ -> setDirectory(newDirectory.getParent()));
                    menuButton.getItems().add(up);
                    Files.list(newDirectory)
                            .filter(Files::isDirectory)
                            .filter(path -> ! path.getFileName().toString().startsWith("."))
                            .sorted(Comparator.comparing((Path path) -> path.getFileName().toString().toLowerCase()))
                            .map(this::menuItemForPath)
                            .forEach(menuButton.getItems()::add);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        setDirectory(initialDirectory);
    }

    private MenuItem menuItemForPath(Path path) {
        MenuItem item = new MenuItem(path.getFileName().toString());
        item.setOnAction( _ -> setDirectory(path));
        return item;
    }

    public Node getView() {
        return menuButton;
    }
}

and a quick test:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        DirectorySelector selector = new DirectorySelector(Paths.get(System.getProperty("user.home")));
        Label current = new Label();
        current.textProperty().bind(selector.directoryProperty().map(Path::toString));
        VBox root = new VBox(10, selector.getView(), current);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(40));
        Scene scene = new Scene(root, 400, 400);
        stage.setScene(scene);
        stage.show();
    }

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

Upvotes: 1

Sai Dandem
Sai Dandem

Reputation: 9979

I think the main issue here is related with the listener to the valueProperty that fires the ActionEvent. Everytime the list is updated the value is reset resulting in firing the ActionEvent again. Below is the related code in ComboBoxListViewSkin:

lh.addChangeListener(control.valueProperty(), e -> {
   updateValue();
   control.fireEvent(new ActionEvent());
});

I believe even if we try to use a boolean field to check in ActionEvent, the the list may be updated but the value of ComboBox is cleared. Not sure if that is the accepted behavior or not.

Lets say, if you want to display the selected value in the ComboBox button cell with the new values in the list and access the selected value from the comboBox.getValue(), you can try the below approach. This may not be the exact use-case for you, but I hope it may give you some direction to start with :).

The general idea is:

  • we only update the list values when the value is not null
  • we keep track(using setUserData) of the selected value to display in the button cell.
  • In the button cell updateItem(), we check for userData and reapply the same value to update in the cell. So that you can access the from comboBox.getValue().

Below is the demo of the approach : enter image description here

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ComboBoxDynamicItemsDemo extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        ComboBox<String> comboBox = new ComboBox<>();
        comboBox.setPrefWidth(150);
        comboBox.getItems().addAll("A","B","C","D","E");
        comboBox.setButtonCell(new ListCell<>() {
            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                if (item != null && !empty) {
                    setText(item);
                } else if(comboBox.getUserData()!=null){
                    comboBox.setValue(comboBox.getUserData().toString());
                }else{
                    setText(null);
                }
            }
        });
        comboBox.setOnAction(e->{
            String value = comboBox.getValue();
            if(value!=null) {
                comboBox.setUserData(value);
                // Simulating the next set of values from the selected value
                List<String> newValues = IntStream.range(1, 6).mapToObj(i -> value + "-" + i).collect(Collectors.toList());
                Platform.runLater(() -> comboBox.setItems(FXCollections.observableArrayList(newValues)));
            }
         });
        Label label = new Label();
        Button print = new Button("Print");
        print.setOnAction(e-> label.setText(comboBox.getValue()));
        HBox root = new HBox(15,comboBox,print,label);
        root.setAlignment(Pos.CENTER);
        Scene scene = new Scene(root, 300,200);
        primaryStage.setScene(scene);
        primaryStage.setTitle("ComboBox Demo");
        primaryStage.show();
    }
}

Upvotes: 1

Related Questions