I have an ObservableMap<String, ObservableSet<String>>.
I would like to create a UI which has a comboBox and a ListView. The comboBox is populated by the keys of the map. Selecting one key from the map would then populate the ListView with the contents of the Set that is mapped to by that key.
In the past I have handled making a ListView for an ObservableSet by creating a second data structure, an ObservableList, and adding a ChangeListener to the set that updates the ObservableList so that it mirrors the set.
However in this case I don't have just one set, but a map of many sets. See my previous question which is similar but simpler: JavaFX: Populate TableView with an ObservableMap that has a custom class for its values
Here is some sample runnable code. It provides most of the functionality I want. However, the ListView doesn't respond to changes in the underlying Map of Sets. In this example, if you select "Vehicles" from the ComboBox and then click the Change Vehicles button, no changes are reflected in the ListView. However if you then select "Colors" and then back to "Vehicles", the ListView is repopulated and you now see the change.
So how would one get the ListView to automatically update itself when the underlying Map of Sets changes? My first guess is that you need to add a Listener to each Set that maintains a mirror of the contents of each Set in an ObservableList. But since this is a Map of Sets, the number of Sets can change and so the number of mirroring Lists will need to change. So I would need to have a collection of ObservableLists, I suppose...? And every time a new element is added to the Map, a new Listener and new ObservableList will need to be constructed.
import java.util.Map;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class MapSetView extends Application {
ObservableMap<String, ObservableSet<String>> map = FXCollections.observableHashMap();
ComboBox<String> keysCombo = new ComboBox<>();
ListView<String> valuesList = new ListView<>();
#Override
public void start(Stage stage) throws Exception {
map.put("Vehicle", FXCollections.observableSet("plane", "train", "automobile"));
map.put("Color", FXCollections.observableSet("black","blue","red"));
keysCombo.getItems().clear();
for(Map.Entry<String, ObservableSet<String>> varEntry : map.entrySet()) {
keysCombo.getItems().add(varEntry.getKey());
}
keysCombo.setOnAction( (ActionEvent e) -> {
String selectedName = keysCombo.getSelectionModel().getSelectedItem();
valuesList.setItems(FXCollections.observableArrayList(map.get(selectedName)));
});
Button changeVehicles = new Button("Change Vehicles");
changeVehicles.setOnAction( (ActionEvent e) -> {
map.get("Vehicle").add("boat");
});
// display UI
VBox vBox = new VBox(8);
vBox.getChildren().addAll(keysCombo, valuesList, changeVehicles);
Scene scene = new Scene(vBox, 400, 400);
stage.setScene(scene);
stage.show();
}
}
Related
I'm trying to let a program react when a line is added in a TableView, but the ItemProperty doesn't notify the listeners when the list is changed.
I think that might be because I don't 'set' the items (as a new different list) but just add to the list, but I'm not sure.
I found this on stackoverflow, but I don't think it's the same problem (or at least the solution didn't work for me).
Is there any convenient way to fix this?
minimal reproducible example:
import javafx.application.Application;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
private static Random RG = new Random();
#Override
public void start(Stage stage) throws Exception {
VBox vBox = new VBox();
TableView<Integer> integerTableView = new TableView<>();
TableColumn<Integer, Integer> testColumn = new TableColumn<>("test");
testColumn.setCellValueFactory(i -> new SimpleIntegerProperty(i.getValue().intValue()).asObject());
integerTableView.getColumns().add(testColumn);
Button button = new Button("Add random number");
button.setOnAction(e -> {
integerTableView.getItems().add(RG.nextInt());
});
vBox.getChildren().addAll(integerTableView, button);
Scene scene = new Scene(vBox);
stage.setScene(scene);
stage.show();
integerTableView.itemsProperty().addListener(e -> System.out.println("Added item"));
}
public static void main(String[] args) {
launch(args);
}
}
Problem & Solution
You're adding the InvalidationListener to the items property, which is an instance of ObjectProperty. A listener on that property will not know when something happens to the value it contains (it doesn't even know if/when its value is observable). You need to add the listener to the ObservableList itself.
// javafx.beans.Observable
integerTableView
.getItems()
.addListener((Observable e) -> System.out.println("Items invalidated!"));
ListChangeListener
Note that an InvalidationListener on an ObservableList will be fired for any kind of change done to the list, and you won't be aware of what kind of change was done. If you want details about the change, then you should use a ListChangeListener.
// javafx.collections.ListChangeListener
integerTableView.getItems().addListener((ListChangeListener<Integer>) c -> {
while (c.next()) {
// process change (see documentation for more info)
}
});
replace
integerTableView.itemsProperty().addListener(e -> System.out.println("Added item"));}
by this
integerTableView.getItems().addListener((ListChangeListener )(e -> {System.out.println("Added item");}));
!!!! you need to cast lambda (ListChangeListener ) otherwise you will get "reference to addlistener is ambiguous " warning , because there are more listeners for that Class
To get an idea of what I want
When the textfield is clicked, the dropdown appears with suggestions that are filtered out as the user types in the text field. The height of the box should also adjust real-time to either contain all of the items, or a maximum of 10 items.
I managed to get this somewhat working using a ComboBox, but it felt a bit rough around the edges and it didn't seem possible to do what I wanted (The dropdown doesn't resize unless you close it and re-open it).
New idea, have a text field and then show a VBox of buttons as the dropdown. The only problem is that I don't know how to position the dropdown so that it doest stay in the noral flow so it can overlay any exisiting elements below the text field. Any ideas?
Please consider this Example, you can take the idea and apply it to your project.
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class SearchFormJavaFX extends Application{
#Override
public void start(Stage ps) throws Exception {
String[] options = {"How do I get a passport",
"How do I delete my Facebook Account",
"How can I change my password",
"How do I write some code in my question :D"};
// note that you don't need to stick to these types of containers, it's just an example
StackPane root = new StackPane();
GridPane container = new GridPane();
HBox searchBox = new HBox();
////////////////////////////////////////////////////
TextField text = new TextField();
// add a listener to listen to the changes in the text field
text.textProperty().addListener((observable, oldValue, newValue) -> {
if(container.getChildren().size()>1){ // if already contains a drop-down menu -> remove it
container.getChildren().remove(1);
}
container.add(populateDropDownMenu(newValue, options),0,1); // then add the populated drop-down menu to the second row in the grid pane
});
// those buttons just for example
// note that you can add action listeners to them ..etc
Button close = new Button("X");
Button search = new Button("Search");
searchBox.getChildren().addAll(text,close,search);
/////////////////////////////////////////////////
// add the search box to first row
container.add(searchBox, 0, 0);
// the colors in all containers only for example
container.setBackground(new Background(new BackgroundFill(Color.GRAY, null,null)));
////////////////////////////////////////////////
root.getChildren().add(container);
Scene scene = new Scene(root, 225,300);
ps.setScene(scene);
ps.show();
}
// this method searches for a given text in an array of Strings (i.e. the options)
// then returns a VBox containing all matches
public static VBox populateDropDownMenu(String text, String[] options){
VBox dropDownMenu = new VBox();
dropDownMenu.setBackground(new Background(new BackgroundFill(Color.GREEN, null,null))); // colors just for example
dropDownMenu.setAlignment(Pos.CENTER); // all these are optional and up to you
for(String option : options){ // loop through every String in the array
// if the given text is not empty and doesn't consists of spaces only, as well as it's a part of one (or more) of the options
if(!text.replace(" ", "").isEmpty() && option.toUpperCase().contains(text.toUpperCase())){
Label label = new Label(option); // create a label and set the text
// you can add listener to the label here if you want
// your user to be able to click on the options in the drop-down menu
dropDownMenu.getChildren().add(label); // add the label to the VBox
}
}
return dropDownMenu; // at the end return the VBox (i.e. drop-down menu)
}
public static void main(String[] args) {
launch();
}
}
What you're trying to do has already been implemented, and is included in ControlsFx. It's open source, and I think it would suit you need. It looks some what like this
You can even add custom nodes to it, so that cross can be done too.
public void pushEmails(TextField Receptient) {
ArrayList<CustomTextField> list = new ArrayList<>();
for (int i = 0; i < Sendemails.size(); i++) {
CustomTextField logo=new CustomTextField(Sendemails.get(i));
ImageView logoView=new ImageView(new Image("/Images/Gmail.png"));
logo.setRight(logoView);
list.add(logo);
}
TextFields.bindAutoCompletion(Receptient, list);
}
In my javafx application I want to create a scene where I can show the usernames of all the users in my database.
Precisely, I want to show a list of labels where every label get a username.
(the number of labels depend on the number of users).
Note: I can do this in java with a list and a foreach loop, but this is the first time that I work with javafx and I want to know how to create a loop of graphic component.
Thanks.
Here are a couple of alternatives, one sample is just looping and adding new labels to the children of a layout pane, the other is using the in-built ListView component. There are other alternatives of course. Which you choose to use will depend upon the functionality you need to achieve.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class UserDisplay extends Application {
#Override public void start(Stage stage) {
String[] users = { "Huey", "Dewey", "Louie" };
VBox layout = new VBox(10);
// ALTERNATIVE 1: add labels in a loop.
for (String user: users) {
Label userLabel = new Label(user);
layout.getChildren().add(userLabel);
}
// ALTERNATIVE 2: use the built-in ListView component.
ListView<String> listView = new ListView<>(
FXCollections.observableArrayList(users)
);
layout.getChildren().add(listView);
layout.setPadding(new Insets(10));
layout.setPrefSize(100,200);
stage.setScene(new Scene(layout));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Basically, here is what I need:
I have a JavaFX ComboBox, and it is set to Editable. Since it is editable, there is a little text field in there where someone can enter in a String. I want to use previously generated data to populate that little text field. How do I accomplish this?
enterSchoolName.setSelectionModel((SingleSelectionModel<String>) FXCollections.observableArrayList(studentData.getSchoolName()));
This is all i have in the way of relevant code and an "attempt" at a solution.
You can set the data items of a ComboBox in the constructor:
ObservableList<String> data = FXCollections.observableArrayList("text1", "text2", "text3");
ComboBox<String> comboBox = new ComboBox<>(data);
or later:
comboBox.setItems(data);
To select a data item, you can select the appropriate index in the SelectionModel or the item itself:
comboBox.getSelectionModel().select(0);
comboBox.getSelectionModel().select("text1");
It's also possible to set a value to the combobox editor, which is not contained in the underlying datamodel:
comboBox.setValue("textXXX");
The "little text field" in a editable ComboBox is known as the editor of the ComboBox. And it's a normal TextField object. To access that object, you need to use the method ComboBox#getEditor(). This way you can use the methods of the TextField class. If I understand you correctly, all you want to do is set the text of that TextField.
This is done by doing comboBox.getEditor().setText(text) or comboBox.setValue(text). Both of these methods will set the text of the ComboBox.
But there's a difference when you want to fetch that text. ComboBox#getValue() ComboBox#getEditor()#getText() doesn't necessarily return the same value.
Consider the following example:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TestComboBox extends Application {
#Override
public void start(Stage stage) {
ComboBox<String> comboBox = new ComboBox<String>();
comboBox.setEditable(true);
comboBox.setValue("Test");
comboBox.getItems().addAll("Test", "Test2", "Test3");
VBox content = new VBox(5);
content.getChildren().add(comboBox);
content.setPadding(new Insets(10));
GridPane valueGrid = new GridPane();
Label cbValue = new Label();
cbValue.textProperty().bind(comboBox.valueProperty());
Label cbText = new Label();
cbText.textProperty().bind(comboBox.getEditor().textProperty());
valueGrid.add(new Label("ComboBox value: "), 0, 0);
valueGrid.add(new Label("ComboBox text: "), 0, 1);
valueGrid.add(cbValue, 1, 0);
valueGrid.add(cbText, 1, 1);
content.getChildren().add(valueGrid);
stage.setScene(new Scene(content));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
If you change the text in the ComboBox by chosing an alternative in the list, both ComboBox#valueProperty() and ComboBox#getEditor#textProperty() changes. But as you can see if you type something into the ComboBox, only the textProperty changes.
So use whichever method you want when you set the text of the ComboBox, but be aware of the difference when you want to retrieve that text.
I'm new at using JavaFX and I'm trying to add an ObservableList to a table view.
The list contains only String.
My goals is to show list of connected devices and let the user choose on which to perform the action (1 or more), is there any better way to achieve this?
Edit:
Ive chaned to ListView and now it shows the list, how can I create a new list from the selected Items ?
Here's an example based on your comments
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class ListSelect extends Application {
#Override
public void start(Stage stage) {
ObservableList<String> items = FXCollections.observableArrayList(
"one","two","three","four","five","six","seven");
ListView<String> list = new ListView<>(items);
ListView<String> selected = new ListView<>();
HBox root = new HBox(list, selected);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
//set this to SINGLE to allow selecting just one item
list.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
list.getSelectionModel().selectedItemProperty().addListener((obs,ov,nv)->{
selected.setItems(list.getSelectionModel().getSelectedItems());
});
}
public static void main(String[] args) {launch(args);}
}
If you are looking for a solution that does not use Java's new Lamba and related features here it is:
In my example the assume the table was set up to work with an arbitrary class Person which defined a first name, last name and email.
//Set the table to multi selection mode
table.getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE);
//Regiseter the listener on the ObervableList<Person>
table.getSelectionModel().getSelectedItems().addListener(multiSelection);
/**A listener for list selections, multiple selections in the TableView**/
ListChangeListener< Person> multiSelection = new ListChangeListener<Person>(){
#Override
public void onChanged( ListChangeListener.Change<? extends Person> changed){
for( Person p : changed.getList())
System.out.println(p);
}
};