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);
}
}
Related
I have made programmatic changes to a Java FX Line Chart and I need a programmatic way to force a re-layout of the JavaFX Chart to occur. This question has been asked/answered before but not in my context.
I have tried the typical methods that have been presented as answers to this question (see complete, minimal example code below with in-line attempts at solving the problem). None of the typical solutions to this problem work.
Specifically (sp is a StackPane):
sp.requestLayout(); // does not work
and
sp.applyCss();
sp.layout(); // does not work
placing the above code in a .runLater() does not work.
I know that my changes are present in the chart because
(1) When I resize the chart by hand my changes suddenly appear
(2) When I use the "resize" method programmatically my changes appear BUT there is a different error (plus only parent nodes are supposed to use the "resize" method - not us programmers).
Below is a minimal complete set of code which reproduces the problem. When you run the code I programmatically change one of the data points to be larger when the chart is displayed. This resize works correctly. When you right-click on the chart a context menu appears with one choice ("Resize ALL the points"). When you select that single option my code resizes all the points - BUT - none of the data points are resized visually. If I resize the chart manually by dragging a side, the chart does a re-layout and all the data node sizes immediately visually change to the correct size (The size I programmatically set them to).
How can I force the re-layout to occur programmatically that I can force to occur manually? I would NOT like to do a hack (like programmatically set the stage size to be 1 pixel smaller and then set it one pixel larger).
Note: I have read that attempts to do a requestLayout() while a layout is in progress will be ignored so perhaps something like that is going on. I think a requestLayout() inside of a runLater() would fix the issue of an ongoing Layout() but that has not worked either.
Update: Scaling was suggested as an alternative to changing the StackPane size. This solution may be helpful to some but not to me. The Look and Feel of scaling a symbol is different than the look and feel of changing the regions size and allowing the "symbol" to grow into that size.
As a complete aside this is my first stackoverflow post. So thanks for all the previous examples a I have used from this forum in the past & thanks in advance for the answer to this problem.
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.LineChart.SortingPolicy;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class dummy extends Application {
#Override
public void start(Stage primaryStage) {
Random random = new Random();
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("X");
yAxis.setLabel("Y");
final LineChart<Number,Number> lineChart = new LineChart<Number,Number>(xAxis,yAxis);
Series<Number,Number> series = new Series<Number,Number>();
series.setName("Dummy Data");
// Generate data
double x = 0.0;
double y = 0.0;
for (int i = 0; i < 10; i++) {
Data<Number,Number> data = new Data(x += random.nextDouble(), y+=random.nextDouble());
series.getData().add(data);
}
lineChart.getData().add(series);
lineChart.setTitle("Random Data");
lineChart.setAxisSortingPolicy(SortingPolicy.NONE);
Scene scene = new Scene(lineChart,1200,600);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
// This resizes the first data point directly (this resize is displayed correctly when program is run)
Node node = series.getData().get(0).getNode();
setSize((StackPane)node,20);
// The context menu is invoked by a right click on the line Chart. It will resize the data point based on a context menu pick
// this resize does not work....unless I resize the window manually which causes a refresh/re-layout of the chart).
lineChart.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (MouseButton.SECONDARY.equals(mouseEvent.getButton())) {
Scene scene = ((Node)mouseEvent.getSource()).getScene();
ContextMenu menu = createMenu(lineChart);
menu.show(scene.getWindow(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
}
}
});
}
private void setSize(StackPane sp, int size) {
sp.setMinSize(size, size);
sp.setMaxSize(size, size);
sp.setPrefSize(size, size);
}
// this creates a context menu that will allow you to resize all the data point nodes
private ContextMenu createMenu(LineChart<Number,Number> lineChart) {
final ContextMenu contextMenu = new ContextMenu();
final MenuItem resize = new MenuItem("Resize ALL the points");
resize.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
StackPane sp = (StackPane)data.getNode();
setSize (sp, 20);
// The above resizes do not take effect unless/until I manually resize the chart.
// the following two calls do not do anything;
sp.applyCss();
sp.layout();
// The request to layout the node does nothing
sp.requestLayout();
// Doing both of the above as runLaters does nothing
Platform.runLater(()->{sp.applyCss();sp.layout();});
Platform.runLater(()->{sp.requestLayout();});
// Going after the parent does nothing either
Group group = (Group)sp.getParent();
group.applyCss();
group.layout();
group.requestLayout();
// Going after the parent in a run later does nothing
Platform.runLater(()->{
group.applyCss();
group.layout();
group.requestLayout();
});
// note... doing a resize [commented out below] will work-ish.
// The documentation says NOT to use it thought that as it is for internal use only.
// By work-ish, the data points are enlarged BUT they are no longer centered on the data point
// When I resize the stage they get centered again - so this "solves" my original problem but causes a different problem
////////////////////////////////////
// sp.resize(20, 20); // Uncomment this line to see how it mostly works but introduces a new issue
////////////////////////////////////
}
}
}
});
contextMenu.getItems().add(resize);
return contextMenu;
}
public static void main(String[] args) {
Application.launch(args);
}
}
You can force a relayout by using e.g. an inner class
class LineChartX<X, Y> extends LineChart<X, Y>
{
public LineChartX(#NamedArg("xAxis") Axis<X> xAxis, #NamedArg("yAxis") Axis<Y> yAxis)
{
super(xAxis, yAxis);
}
#Override
public void layoutPlotChildren()
{
super.layoutPlotChildren();
}
}
and calling
lineChart.layoutPlotChildren();
in your menu action.
Simple one-line Solution:
nodes in LineChart scene graph have these parent-child relationships:
Pane chartContent - Group plotArea - Group plotContent - Path seriesLine;
layout requests for Group plotArea, defined in class XYChart, are suppressed:
private final Group plotArea = new Group(){
#Override public void requestLayout() {} // suppress layout requests
};
but Pane chartContent accepts layout requests:
Node node = series.getNode();
if (node instanceof Path) {
Path seriesLine = (Path) node;
Parent parent = seriesLine.getParent();
if (parent instanceof Group) {
Group plotContent = (Group) parent;
parent = plotContent.getParent();
if (parent instanceof Group) {
Group plotArea = (Group) parent;
parent = plotArea.getParent();
if (parent instanceof Pane) {
Pane chartContent = (Pane) parent;
chartContent.requestLayout();
}
}
}
}
so relayout of your chart can be forced by addding this single line
series.getNode().getParent().getParent().getParent().requestLayout();
to the end of your menu action handler.
You don't need to cast that node into a StackPane and set sizes. You need to use the setScaleX() and setScaleY() Methods
Node node = series.getData().get(0).getNode();
node.setScaleX(20);
node.setScaleY(20);
#Override
public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
Node node = data.getNode();
node.setScaleY(20);
node.setScaleX(20);
}
}
}
#c0der posted a solution in comment form to my original post which worked but produced a runtime warning (in Eclipse). His solution was to add a dummy style sheet at the lineChart level with lineChart.getStylesheets().add(""); after the for loops ended. This code produced the warning "Apr 28, 2020 9:01:12 AM com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged WARNING: Resource "" not found."
What did work without causing a run-time warning was to load an empty .css file and add it as a StyleSheet:
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm());
// note: I keep my .css resource files at the same location as my CSS class
// which is why I have the code "CSS.class" above
This one line solution worked once but I doubted it would work multiple times. I tested it to by increasing the size of the StackPane by 5 for each successive time "Resize ALL the points" was selected (in my dummy code above). Sure enough, it only worked the first time.
However, I added the no-op code lineChart.getStylesheets().replaceAll((s)->s+" "); before that one line solution and now it works multiple times in a row.
No matter how many times I executed the two lines of code
lineChart.getStylesheets().replaceAll((s)->s+" ");
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm()); `
it (1) worked and (2) The size of the list of lineChart StyleSheets did not grow beyond a size of 1. So a solution with a mystery.
Note: if you have an existing style sheet (I did not in my dummy example above) lineChart.getStylesheets().replaceAll((s)->s+" "); by itself may work as well. For some reason lineChart.getStylesheets().replaceAll((s)->s); without adding the " " on the end did not work.
Note: I originally thought I would have to code up a toggle solution to add Empty.css and the remove Empty.css with alternate calls but that proved unnecessary.
Bottom Line: if you have an existing StyleSheet lineChart.getStylesheets().replaceAll((s)->s+" "); works. If you do not have an existing StyleSheet adding an empty .css file as a StyleSheet in conjunction with the above replaceAll works.
Thanks again to #c0der for his original approach.
Reproduced in OpenJFX 11.0.2 & 12.0.1 SDK (Windows 10, x64), not reproducible in JavaFX 8
Right-click on a table-column, then try to resize the column. No resize cursor is shown and column can't be resized until you manually click on the column again.
Any ideas for a workaround? I need to usecontextMenu for TableColumns, so potential workarounds that make the header ignore right mouse click aren't possible.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class Foo extends Application {
#Override
public void start(Stage stage) throws Exception {
TableView<Object> testView = new TableView<>();
testView.getColumns().addAll(new TableColumn<Object, Object>("C1"), new TableColumn<Object, Object>("C2"), new TableColumn<Object, Object>("C3"));
stage.setScene(new Scene(testView));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Ok I found the following (very, very dirty) workaround. I never tried this before because I assumend it would prevent the context menu from showing (as I noted in my original question), but apprently simply consuming the mouse event of every TableColumnHeader works and the context menu is still shown correctly (also works with TableColumns without context menus).
Not sure if anything internal could go wrong with this, but as the right click doesn't seem to be doing anything useful by default, I hope not.
Of course lookupAll needs to be called after it has been rendered.
Note 1: If you have TableMenuButtonVisible set to true, you need to do this every time a column is set to visible.
Note 2: Its getting dirtier and dirtier. Simply calling this again after a column has been set to visible (see note 1) doesn't always suffice (also not with a Platform.runLater call). I assume that's because the column header hasn't been rendered at that point. You either
need to wait until the Set<Node> is fully filled, i.e. the size of
it must be amountOfVisibleColumns + 1. If its equal to the amount
of visible columns, it won't work for the newly shown column.
or call layout() on the TableView before lookupAll
or if you have a class that extends TableView, override layoutChildren and execute the lookup if the amount of visible columns has changed
Note 3: You need to keep track of the old onMousePressed and execute it if the button isn't SECONDARY, otherwise the reordering of columns won't work.
Please let me know if you can think of any cleaner way.
import java.util.Set;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.TableColumnHeader;
import javafx.scene.input.MouseButton;
import javafx.stage.Stage;
public class Foo extends Application {
#Override
public void start(Stage stage) throws Exception {
TableView<Object> testView = new TableView<>();
testView.getColumns().addAll(createColumn("C1"), createColumn("C2"), createColumn("C3"));
stage.setOnShown(ev -> {
Set<Node> headers = testView.lookupAll("TableColumnHeader");
for (Node header : headers) {
if (header != null) {
((TableColumnHeader) header).setOnMousePressed(e -> {
if (e.getButton() == MouseButton.SECONDARY) {
e.consume();
}
});
}
}
});
stage.setScene(new Scene(testView));
stage.show();
}
private TableColumn<Object, Object> createColumn(String text) {
MenuItem item = new MenuItem("Context");
item.setOnAction(e -> {
System.out.println("Action");
});
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().add(item);
TableColumn<Object, Object> column = new TableColumn<>(text);
column.setContextMenu(contextMenu);
return column;
}
public static void main(String[] args) {
launch(args);
}
}
EDIT: Found the described bug in the Java bug tracker and filed a PR with the fix:
https://github.com/openjdk/jfx/pull/483
EDIT 2: My PR was accepted and merged back. The bug is fixed now, you can test it by using 17-ea+11. :-)
I have the same problem. This bug is caused by the mousePressedHandler added in TableColumnHeader. This class has even more problems, for example if I close a PopupControl with setConsumeAutoHidingEvents(true) by a click on a column, the sorting will be triggered. Those methods needs to be changed, maybe the addEventHandler methods should be used instead of the convenience setOn... methods.
I fixed it by consuming the event when I'm about to show my PopupControl:
public class MyTableColumnHeader extends TableColumnHeader {
public MyTableColumnHeader(TableColumnBase tc) {
super(tc);
addEventHandler(MouseEvent.MOUSE_PRESSED, this::onMousePressed);
}
private void onMousePressed(MouseEvent mouseEvent) {
if (mouseEvent.getButton() == MouseButton.SECONDARY) {
showPopup();
// Consume here, so the column won't get 'stuck'.
mouseEvent.consume();
}
}
private void showPopup() {
...
}
}
Eventually, someone should open at least a bug. I may will also have a look in the not too distant future.
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());
}
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).
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.