JavaFX Listview Observe Map - java

I have a HashMap. I want to display the Keys in a ListView.
The trouble is, ListView.setItems() wants an ObservableList, and all I have is a keySet().
How can I get a ListView to observe the keys in my Map, without doing something clunky like maintaining two matching data structures?

I know this is not the answer you want, but... my advice is to maintain two data structures.
Sample App (synching data structures)
Add items to a map using the UI on the right and, as the keys in the extension -> mimeType map change, you will see the list of keys shown in the ListView on the left automatically update.
The solution listens for changes to an ObservableMap which wraps the extension -> mimetype map and, when a key in the map changes, applies relevant updates to an ObservableList which backs the ListView.
In the sample screenshot below, png will be added to the left hand side list when the user presses the Add button.
import javafx.application.Application;
import javafx.collections.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
public class ObservableMapTest extends Application {
// map initializer based on http://stackoverflow.com/a/25829097/1155209
private static final Map<String, String> extensionToMimeMap =
Arrays.stream(new String[][]{
{"txt", "text/plain"},
{"html", "text/html"},
{"js", "application/javascript"},
{"css", "text/css"}
}).collect(Collectors.toMap(kv -> kv[0], kv -> kv[1]));
#Override
public void start(Stage stage) throws Exception {
// create an observable wrapper for our map data.
final ObservableMap<String, String> observableExtensionToMimeMap = FXCollections.observableMap(
extensionToMimeMap
);
// create an ListView based on key items in the map.
ListView<String> extensionListView = new ListView<>();
extensionListView.getItems().setAll(extensionToMimeMap.keySet());
extensionListView.setPrefWidth(100);
// have the ListView observe the underlying map and modify its items if the key set changes.
observableExtensionToMimeMap.addListener((MapChangeListener<String, String>) change -> {
extensionListView.getItems().removeAll(change.getKey());
if (change.wasAdded()) {
extensionListView.getItems().add(change.getKey());
}
});
// layout the app.
Pane layout = new HBox(
extensionListView,
createAddExtensionPane(
observableExtensionToMimeMap
)
);
layout.setPrefHeight(150);
// display the app.
stage.setScene(new Scene(layout));
stage.show();
}
/** Helper factory function to create a UI for adding an element to an map. */
private GridPane createAddExtensionPane(Map<String, String> map) {
GridPane addExtensionPane = new GridPane();
addExtensionPane.add(new Label("Extension:"), 0, 0);
TextField extensionField = new TextField();
addExtensionPane.add(extensionField, 1, 0);
addExtensionPane.add(new Label("Mime Type:"), 0, 1);
TextField mimeTypeField = new TextField();
addExtensionPane.add(mimeTypeField, 1, 1);
Button addButton = new Button("Add");
addButton.setOnAction(event ->
map.put(
extensionField.getText(),
mimeTypeField.getText()
)
);
addExtensionPane.add(addButton, 1, 2);
addExtensionPane.setPadding(new Insets(10));
addExtensionPane.setHgap(5);
addExtensionPane.setVgap(10);
return addExtensionPane;
}
public static void main(String[] args) {
launch(args);
}
}
A possible alternate implementation which did not copy data into an ObservableList would be to implement the ObservableList interface and in the implemented methods refer directly to the key data of the observable map. Such an approach would be very complex to implement and not worthwhile pursuing (IMO).

Related

Problem with extending SimpleStringProperty

I am just trying to extend a SimpleStringProperty in OpenJFX 11.0.1 to add some extra functionality. But ist seems not so easy, I experienced strange behavior of my extended Property and I don't know why. I think it should work.
My in this sample code simplified SimpleStringProperty extension contains another readonly string property which should be updated every time the the user types into a bound TextField. In this case remove all not allowed characters and convert the prefix. (I know this is not perfect but short enough to show)
After starting the sample code you will get a window with a rows of Controls. Typing in a String like "001 (242) 555666" the label should show the normalized phone number like "+1242555666".
The initial conversion works correcty.
I never get any exceptions.
The conversion is called when I type in new digits.
But if you play around with typing and deleting after a few seconds the set() method of my property isn't longer triggered by the bidirectional binding to the TextField.
To simplify the example I didn't use a TextFormatter. If I use one the problem doesn't change.
Can anyone help me figure out the problem?
Windows and OS X show the same behavior with OpenJFX 11 and OpenJFX 11.0.1
I tried the same code with JDK 1.8 and there it works fine.
package testproperty;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class TestProperty extends Application {
// attempt to create an own property
public class myPhoneNumberProperty extends SimpleStringProperty {
private final ReadOnlyStringWrapper normalizedNumber = new ReadOnlyStringWrapper("");
public ReadOnlyStringProperty normalizedNumberProperty() { return normalizedNumber.getReadOnlyProperty(); }
public String getNormalizedNumber() { return normalizedNumber.get(); }
public myPhoneNumberProperty() {
super();
}
public myPhoneNumberProperty(String s) {
super(s);
calculate();
}
#Override
public void set(String s) {
super.set(s);
calculate();
}
private void calculate() {
// some calculations (only for test purposes)
String original = this.get();
String result = original.replaceAll("[^0123456789]","");
if (result.startsWith("00")) result = result.replaceFirst("00", "+");
if (original.startsWith("+")) result = "+".concat(result);
normalizedNumber.set(result);
}
}
#Override
public void start(Stage primaryStage) {
// create my property
myPhoneNumberProperty phoneNumberA = new myPhoneNumberProperty("+34 952 111 222");
// set up grid pane
GridPane grid = new GridPane();
grid.setPadding(new Insets(5,5,5,5));
grid.setVgap(20);
grid.setHgap(20);
// set up the row
Label labelA = new Label("Enter phone number");
TextField textFieldA = new TextField();
textFieldA.textProperty().bindBidirectional(phoneNumberA);
Label labelB = new Label("Normalized number");
Label labelN = new Label();
labelN.textProperty().bind(phoneNumberA.normalizedNumberProperty());
grid.addRow(0, labelA, textFieldA, labelB, labelN);
// complete scene
Scene scene = new Scene(grid, 1000, 100);
primaryStage.setTitle("PhoneNumberProperty TestProg");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Your phoneNumberA property object is being garbage collected. To fix this you must keep a strong reference to the object. One option is to make it an instance field.
JavaFX implements bindings using weak listeners/references. Bidirectional bindings have no strong references to the other property. This is different from unidirectional bindings where a reference to the observable value must be kept in order to unbind from it later.

JavaFX ComboBox becomes unclickable after removal and re-adding

I think I perhaps have found a bug in Java, or maybe I am doing something wrong.
I populate a container based on some received data. The container has one or more ComboBoxes. On ComboBox selection change I receive new data. I then clear the GridPane and re-add the nodes (that still exist in the new data, and/or add new nodes).
The ComboBox still has focus, but I am unable to activate it again on click. Anything which causes the ComboBox to lose focus (such as focusing another component) will cause it to work again.
This is an simplified example. Tried with jdk1.8.0_162 and jdk-9.0.4
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ComboBoxTest extends Application {
public static void main(String[] args) {
ComboBoxTest.launch(args);
}
#Override
public void start(Stage stage) {
VBox root = new VBox();
final ComboBox<String> choices = new ComboBox<>();
choices.getItems().add("Test1");
choices.getItems().add("Test2");
root.getChildren().add(choices);
choices.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> {
root.getChildren().clear();
root.getChildren().add(choices);
});
Platform.setImplicitExit(true);
stage.setScene(new Scene(root));
stage.show();
}
}
The design is dynamic. I have a list of values received from a server. This is used to create and place ComboBox on a grid. When the user changes a selection in a ComboBox it receive a new list of values from the server. This list may still contain values that corresponds to existing nodes in the grid. They are reused rather than re-created.
Just to not loose reason and solution posted as comment to the deleted answer by sillyfly (post your own and I'll delete this :)
A little guess as to the underlying cause/issue - the change causes the ComboBox to disappear while its list (which is technically a different stage) is showing. My guess is that leaves it in an indefinite state where it thinks the list is still showing, but it never hides so it doesn't reset. In this case, maybe calling ComboBox::hide will also work
This assumption is correct as you can see if you change the selection by keyboard (in which case the dropdown is not open): the combo is still accessible by keyboard and mouse. So hiding the dropdown before removing indeed is the solution.
In code (the simplified example in the Michael's edit)
public class ReaddFocusedCombo extends Application {
#Override
public void start(Stage stage) {
VBox root = new VBox();
final ComboBox<String> choices = new ComboBox<>();
choices.getItems().add("Test1");
choices.getItems().add("Test2");
root.getChildren().add(choices);
choices.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
// guess by sillyfly: combo gets confused if popup still open
choices.hide();
root.getChildren().clear();
root.getChildren().add(choices);
// suggested in answer: working but then the choice isn't focused
//root.requestFocus();
// doesn't work
// choices.requestFocus();
});
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Update: a little search in the bug parade turned up a similar misbehaviour on adding a showing combo which was fixed on initial attaching to a scene, but missed the dynamic use case. Filed a new issue for the latter.

Hiding Legend for Zero Value Chart Items javaFX

I'm new to the forums, so I hope I'm not asking a question that has been answered in the past. I've tried to be thorough looking for answer before posting.
I am currently working on a pie chart that will eventually be used for tracking financial expenses. Right now I have several categories that make up each slice. I am trying to hide the legend for the zero value slices.
I am doing this in javaFX. I'm still very green when it comes to programming and don't have experience outside of Java. Any help as explained to dummies would be appreciated. Thanks.
Added a picture and complete code to illustrate the problem at hand. Restaurants & Dining, and Shopping & entertainment both have zero values. I want to hide the legend for those items in this example.
package Example;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.PieChart;
public class PieExampleController implements Initializable {
#FXML
private PieChart pieChart;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
// TODO Auto-generated method stub
ObservableList<PieChart.Data> pieChartData = FXCollections.observableArrayList(
new PieChart.Data("Groceries", 1),
new PieChart.Data("Transportation", 1),
new PieChart.Data("Restaurants & Dining", 0),
new PieChart.Data("Shopping & Entertainment", 0));
pieChart.setData(pieChartData);
}
}
Thats how i do it:
List<PieChart.Data> dataArrayList = new LinkedList<Data>();
if (value1>0) {
Data data = new PieChart.Data("my label", value1);
dataArrayList.add(data);
}
...
ObservableList<PieChart.Data> pieChartData =
FXCollections.observableArrayList(dataArrayList);
Adding only not empty data entries (or removing empty entries) manually once at startup is just fine if the data is immutable and unmodifiable. On the other hand, if it can change during the lifetime of the chart, we need a mechanism that handles the add/remove automagically: FilteredList to the rescue.
Below is an example that
configures a source list with an extractor (on the pieValueProperty): doing so will notify any listChangeListener on change of that value with a change of type update
wraps a FilteredList around the source list
configures the pieChart with the filteredList
With that in place, we can install a predicate on the filteredList that hides items as needed: the example uses a Slider to update the lower threshhold of which data values should be included in the chart.
Unfortunately, PieChart has a couple of bugs (sigh... whatever I touch in FX, they always boil up ...) that interfere with such a simple setup
due to a freaky mixture of node/value plus "optimized" internal data structure plus incorrect implementation of syncing the internal (linked) data structure with changes to the list the chart can't be animated
the sync can't handle changes of type replaced at all (which is what FilteredList fires on resetting the predicate)
In an example both issues can be avoided by disabling animation and clearing out the list (set a predicate that blocks all) before setting the real condition. In producation code such tweaking may or may not be possible.
The example:
public class FilteredPieChartExample extends Application {
#Override
public void start(Stage primaryStage) {
FilteredList<Data> filtered = getChartData();
//ListChangeReport report = new ListChangeReport(filtered);
PieChart pieChart = new PieChart(filtered);
// bug in pieChart: can't handle data modification with animation on
pieChart.setAnimated(false);
// use slider to set lower threshhold for value of data to show in pie
Slider slider = new Slider(-1., 100., -1.);
slider.valueProperty().addListener((src, ov, nv) -> {
// actually, cannot handle data modification at all ... need to clear out first ...
// bug in pieChart.dataChangeListener: doesn't handle replaced correctly
filtered.setPredicate(data -> false);
filtered.setPredicate(data -> data.getPieValue() > nv.doubleValue());
//report.prettyPrint();
});
primaryStage.setTitle("PieChart");
Pane root = new VBox(pieChart, slider);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private FilteredList<Data> getChartData() {
// use ObservableList with extractor on pieValueProperty
ObservableList<Data> answer = FXCollections.observableArrayList(
e -> new Observable[] {e.pieValueProperty()}
);
answer.addAll(
new Data("java", 17.56),
new Data("C", 17.06),
new Data("C++", 8.25),
new Data("C#", 8.20),
new Data("ObjectiveC", 6.8),
new Data("PHP", 6.0),
new Data("(Visual)Basic", 4.76),
new Data("Other", 31.37),
new Data("empty", 0)
);
return new FilteredList<>(answer);
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(FilteredPieChartExample.class
.getName());
}

Multiple Boolean Binding in JavaFX

I am attempting to bind a checkbox to multiple checkbox as seen below:
private void bindPanelToPackages(CheckBox panel, CheckBox ...pkg){
BooleanProperty panelBinding = null;
BooleanBinding binder = null;
for(CheckBox p: pkg){
if(panelBinding == null){
panelBinding = p.selectedProperty();
}
else{
binder = panelBinding.and(p.selectedProperty());
}
}
if(binder != null){
panel.selectedProperty().bind(binder);
}
else if(panelBinding != null){
panel.selectedProperty().bindBidirectional(panelBinding);
}
}
What I want is to allow bidirectional group bindings when 'pkg' has more than one item. That way when I select my packages, the 'panel' will automatically be selected or if I select 'panel', all the 'pkg' will be selected/deselected. I got stuck at :
panel.selectedProperty().bind(binder);
and got
"JavaFX Application Thread" java.lang.RuntimeException: CheckBox.selected : A bound value cannot be set.
since I did a one directional binding for 'binder'. Is there a way I can perform something equivalent to this?:
panel.selectedProperty().bindBidirectional(binder);
I can't seem to find it in the docs or I'm not looking at the right places. Thanks!
The condition "all check boxes are selected" can only be expressed as a BooleanBinding, not as a BooleanProperty. Basically, the issue is that making that condition false is not clearly defined: there are many ways to do it (i.e. make any non-empty subset of all the checkboxes unselected). Hence you cannot use bidirectional bindings: you have to use listeners on each of the two conditions.
Here is one implementation:
// must keep a reference to the Binding to prevent premature
// garbage collection:
BooleanBinding allSelected ;
private void bindPanelToPackages(CheckBox pane, CheckBox... packages) {
// BooleanBinding that is true if and only if all check boxes in packages are selected:
allSelected = Bindings.createBooleanBinding(() ->
// compute value of binding:
Stream.of(packages).allMatch(CheckBox::isSelected),
// array of thing to observe to recompute binding - this gives the array
// of all the check boxes' selectedProperty()s.
Stream.of(packages).map(CheckBox::selectedProperty).toArray(Observable[]::new));
// update pane's selected property if binding defined above changes
allSelected.addListener((obs, wereAllSelected, areAllNowSelected) ->
pane.setSelected(areAllNowSelected));
// use an action listener to listen for a direct action on pane, and update all checkboxes
// in packages if this happens:
pane.setOnAction(e ->
Stream.of(packages).forEach(box -> box.setSelected(pane.isSelected())));
}
and a SSCCE:
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class MultipleCheckBoxSelection extends Application {
private BooleanBinding allSelected ;
#Override
public void start(Stage primaryStage) {
CheckBox selectAll = new CheckBox("Select all");
int numBoxes = 5 ;
CheckBox[] boxes = IntStream
.rangeClosed(1, numBoxes)
.mapToObj(i -> new CheckBox("Item "+i))
.toArray(CheckBox[]::new);
bindPanelToPackages(selectAll, boxes);
VBox root = new VBox(10, selectAll);
root.setStyle("-fx-padding: 15;");
Stream.of(boxes).forEach(box -> box.setStyle("-fx-padding: 0 0 0 10;"));
Stream.of(boxes).forEach(root.getChildren()::add);
Scene scene = new Scene(root, 250, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private void bindPanelToPackages(CheckBox pane, CheckBox... packages) {
// BooleanBinding that is true if and only if all check boxes in packages are selected:
allSelected = Bindings.createBooleanBinding(() ->
// compute value of binding:
Stream.of(packages).allMatch(CheckBox::isSelected),
// array of thing to observe to recompute binding - this gives the array
// of all the check boxes' selectedProperty()s.
Stream.of(packages).map(CheckBox::selectedProperty).toArray(Observable[]::new));
// update pane's selected property if binding defined above changes
allSelected.addListener((obs, wereAllSelected, areAllNowSelected) ->
pane.setSelected(areAllNowSelected));
// use an action listener to listen for a direct action on pane, and update all checkboxes
// in packages if this happens:
pane.setOnAction(e ->
Stream.of(packages).forEach(box -> box.setSelected(pane.isSelected())));
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX Choiceox change not updating graphics

When I change the underlying observable array list the graphics choice box doesn't update. There must be a newer solution than what I have seen suggested here for example:
JavaFX: Update of ListView if an element of ObservableList changes
int selected = productsChoiceBox.getSelectionModel().getSelectedIndex();
Product prod = products.get(selected);
prod.setName(productName.getText());
prod.setUrl(productUrl.getText());
Any thoughts? I would like to avoid removing and adding.
The "standard" answer is to use an ObservableList with an extractor. However, when I tested this out, it didn't behave as advertised, and it seems like there is a bug (my guess is that ChoiceBox is not correctly handling wasUpdated type changes fired in its ListChangedListener) which I will report at JIRA. Update: filed report at https://javafx-jira.kenai.com/browse/RT-38394
The factory method FXCollections.observableArrayList(Callback) creates an (empty) observable array list. The provided Callback is a function that maps each element in the list to an array of Observables. The list registers listeners with those observables, and if those properties change, the list fires update notifications to its listeners.
This produces strange results with a ChoiceBox, however; one possible workaround would be to use a ComboBox which seems to work fine.
Here's some sample code. Select an item: then type in the text field and press enter to change the name of the selected item. Change ChoiceBox to ComboBox to see the correct behavior:
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ChoiceBoxUpdateExample extends Application {
#Override
public void start(Stage primaryStage) {
ChoiceBox<Item> choiceBox = new ChoiceBox<>();
ObservableList<Item> items = FXCollections.observableArrayList(
item -> new Observable[] {item.nameProperty()}); // the extractor
items.addAll(
IntStream.rangeClosed(1, 10)
.mapToObj(i -> new Item("Item "+i))
.collect(Collectors.toList()));
choiceBox.setItems(items);
TextField changeSelectedField = new TextField();
changeSelectedField.disableProperty()
.bind(Bindings.isNull(choiceBox.getSelectionModel().selectedItemProperty()));
changeSelectedField.setOnAction(event ->
choiceBox.getSelectionModel().getSelectedItem().setName(changeSelectedField.getText()));
BorderPane root = new BorderPane();
root.setTop(choiceBox);
root.setBottom(changeSelectedField);
Scene scene = new Scene(root, 250, 150);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Item {
public final StringProperty name = new SimpleStringProperty();
public StringProperty nameProperty() {
return name ;
}
public final String getName() {
return nameProperty().get();
}
public final void setName(String name) {
nameProperty().set(name);
}
public Item(String name) {
setName(name);
}
#Override
public String toString() {
return getName();
}
}
public static void main(String[] args) {
launch(args);
}
}
The correct and proper answer is from James_D, but if you REALLY want to use ChoiceBox, then try adding and removing:
int selected = productsChoiceBox.getSelectionModel().getSelectedIndex();
products.remove(selected);
products.add(selected, prod);
I do NOT believe this is the right way, but I tested it, and it does work. The ChoiceBox stays on the the removed and selected index and looks like it updates.

Categories