Reputation: 316
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
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
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:
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 :
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